Compare commits

...

4 Commits

Author SHA1 Message Date
6a2cb820bc fix(versioning): 修复浅备份文件修改归属错误及列表/详情不一致 2026-07-09 18:45:40 +08:00
765196ea3c feat(skills): host 模式 skill 归档目录迁移到运行态 agentskills 2026-07-09 18:42:42 +08:00
3db4e0beb4 feat(sandbox): macOS host sandbox deny-list for sensitive paths
- Add default deny_read_paths/deny_read_regexes to host_sandbox_policy.json
- Generate deny rules for ~/.ssh, ~/.aws, ~/.kube, keychains, .env, etc.
- Readonly sandbox: global read + deny sensitive + write only /dev/null
- Workspace-write sandbox: global read + deny sensitive + writable paths
- Keep workspace files readable by re-allowing workspace subpath after deny
- Backend API validates path authorization against deny list
- Path auth dialog hint explains readable paths behavior
2026-07-09 16:25:04 +08:00
15106e7d00 feat(branding): 将产品名统一为 Astrion
- 前端页面标题、登录/注册页、左侧栏、聊天头部、教程欢迎语全部改为 Astrion
- CLI 欢迎面板与状态栏改为 Astrion CLI
- 后端启动日志与 setup 向导改为 Astrion
- package.json 与 README 标题同步更新
- 主 system prompt 增加 Astrion 自我介绍,视觉模型 prompt 文件名从 qwenvl 改为 vl
- 项目记忆与 AGENTS.md/CLAUDE.md 新增产品名说明
2026-07-07 18:17:48 +08:00
38 changed files with 425 additions and 102 deletions

View File

@ -0,0 +1,48 @@
---
name: astrion_naming
description: 当需要了解或引用本项目的品牌名称、产品名 Astrion 的含义与来源时,应该索引本记忆
---
# 项目名称 / 产品名Astrion
## 决策记录
- **暂定名称**Astrion
- **决策时间**2026-07-07
- **命名方向**:人格化 + 宇宙现象/天体 + 科技意象
- **当前状态**暂定后续如需变更请同步更新本记忆、AGENTS.md 与 claude.md
## 名称含义
**构词**
- **Astr-**:来自希腊语 *astron*,意为“星星 / 天体 / 宇宙”
- **-ion**:科技命名中常见的粒子/状态后缀(如 photon、electron、neutron、ion
**字面意象**
> 星之子、星际粒子、来自宇宙尘埃的微小发光体。
## 表达特点
| 特点 | 解读 |
|------|------|
| 科技感 | `-ion` 后缀自带物理/科技联想 |
| 宇宙感 | `Astr-` 词根直接指向星空 |
| 轻盈感 | 粒子暗示微小、快速、自由穿行 |
| 能量感 | ion离子带电暗示活跃、驱动、连接 |
| 可人格化 | 三音节 `/ˈæstriən/`,发音接近 Orion、Damion适合作为“他/她” |
| 中性 | 不偏男性或女性,适用面广 |
## 品牌叙事参考
1. **连接者**:像星际间传递信号的粒子,把人类意图传递到工具、文件、终端、子智能体之间。
2. **从混沌中组织秩序**宇宙尘埃在引力下聚集成恒星Astrion 把零散任务聚合成可执行结果。
3. **渺小但明亮**:单个粒子微不足道,但携带能量、能引发连锁反应——对应 Agent 虽小,却能撬动复杂系统。
## 中文参考
- 全称音译:**阿斯特里恩**
- 简称候选:**星子**(对应 astron + particle更顺口
## 同步位置
- 项目规范:`AGENTS.md`
- Claude Code 指南:`claude.md`

View File

@ -7,6 +7,16 @@
> 和用户交流时默认使用中文。
## 0) 项目名称
本项目(仓库 `agents`)的暂定产品名为 **Astrion**
- **构词**`Astr-`(希腊语 *astron*,星星/天体)+ `-ion`(粒子/状态后缀,如 photon、electron
- **意象**:星之子、星际粒子、来自宇宙尘埃的微小发光体。
- **寓意**Agent 像星际间传递信号的粒子,把人类意图传递到工具、文件、终端与子智能体之间;同时把零散任务像星尘聚合成恒星一样,聚合成可执行结果。
- **状态**:暂定名,后续若变更需同步更新 `AGENTS.md`、`claude.md` 与项目记忆 `astrion_naming`
- **中文参考**:阿斯特里恩;简称可考虑 **星子**
## 1) 当前项目结构(按代码现状)
- **后端入口**

View File

@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
多用户 AI Agent 系统,为每个登录用户提供独立的 Docker 容器环境支持终端交互、文件操作、对话管理和实时监控。主要用于学习和实验"智能体 + 真实 Dev Workflow",代码大量由 AI 生成。
多用户 AI Agent 系统,产品暂定名为 **Astrion**`Astr-` 星星/天体 + `-ion` 粒子后缀,意象为“星之子、星际粒子”)。为每个登录用户提供独立的 Docker 容器环境支持终端交互、文件操作、对话管理和实时监控。主要用于学习和实验"智能体 + 真实 Dev Workflow",代码大量由 AI 生成。
## Core Architecture

View File

@ -1,4 +1,4 @@
# AI Agent 系统
# Astrion
一个功能完整的 AI 智能体系统,支持多模态交互、子智能体协作、实时终端操作和丰富的工具集成。

View File

@ -1,15 +0,0 @@
---
name: Test Skill
description: Validate skills loading and read_file flow.
example_prompts:
- "Use the test skill to explain how to proceed."
tools:
- read_file
tags:
- test
---
# Test Skill
This skill exists to verify that the skills system can discover, list, sync,
and read SKILL.md content via read_file. Replace this content later.

View File

@ -97,7 +97,7 @@ function isWideCodePoint(code: number): boolean {
export function WelcomePanel({ status }: { status: CliStatus }) {
return (
<Box flexDirection="column" marginTop={1}>
<Text color="gray">Agents</Text>
<Text color="gray">Astrion</Text>
<Box borderStyle="round" borderColor="gray" paddingX={2} paddingY={1} flexDirection="column">
<Text bold>Welcome back!</Text>
<Text color="gray">{status.model} · {status.runMode}</Text>
@ -279,7 +279,7 @@ export function WorkspacePrompt({ cwd, selectedIndex }: { cwd: string; selectedI
export function StatusPanel({ status }: { status: CliStatus }) {
return (
<Box borderStyle="round" borderColor="gray" paddingX={1} flexDirection="column" marginTop={1}>
<Text>{'>_ Agents CLI'}</Text>
<Text>{'>_ Astrion CLI'}</Text>
<Text> </Text>
<StatusField label="Model" value={status.model} />
<StatusField label="Directory" value={shortPath(status.directory)} />

View File

@ -1,4 +1,28 @@
{
"macos_writable_paths": [],
"macos_readable_extra_paths": []
"macos_readable_extra_paths": [],
"macos_deny_read_paths": [
"~/.ssh",
"~/.aws",
"~/.azure",
"~/.gcp",
"~/.google",
"~/.kube",
"~/.docker",
"~/.gnupg",
"~/.npmrc",
"~/.netrc",
"~/.pypirc",
"~/.git-credentials",
"~/.bash_history",
"~/.zsh_history",
"~/.psql_history",
"~/.mysql_history",
"~/.pgpass",
"/Library/Keychains",
"~/Library/Keychains"
],
"macos_deny_read_regexes": [
"^/.*\\.env(\\.[^/]*)?$"
]
}

View File

@ -105,6 +105,9 @@ USER_SPACE_DIR = _runtime_dir("USER_SPACE_DIR", "users")
# API 专用用户与工作区(与网页用户隔离)
API_USER_SPACE_DIR = _runtime_dir("API_USER_SPACE_DIR", "api/users")
# ── 用户自定义 skill 归档目录host 模式全局docker/web 模式可与 per-user 并存)──
CUSTOM_SKILLS_DIR = str(Path(RUNTIME_ROOT) / _MODE / "agentskills")
# ── 是否宿主机模式 ──
IS_HOST_MODE = _MODE == "host"
@ -190,6 +193,7 @@ __all__ = [
"INVITE_CODES_FILE",
"ADMIN_POLICY_FILE",
"API_USER_SPACE_DIR",
"CUSTOM_SKILLS_DIR",
"API_USERS_DB_FILE",
"API_TOKENS_FILE",
"API_USAGE_FILE",

View File

@ -112,13 +112,13 @@ class MessagesMixin:
# 根据当前模型多模态能力选择系统提示。system prompt 按对话冻结;
# 工具 schema 保持动态,由 define_tools 每轮根据真实状态生成。
current_model = getattr(self, "model_key", None)
prompt_name = "main_system_qwenvl" if (model_supports_image(current_model) or model_supports_video(current_model)) else "main_system"
prompt_name = "main_system_vl" if (model_supports_image(current_model) or model_supports_video(current_model)) else "main_system"
model_key = getattr(self, "model_key", None)
prompt_replacements = get_model_prompt_replacements(model_key)
def _build_main_system_prompt() -> str:
system_prompt_template = self.load_prompt(prompt_name)
# main_system.txt / main_system_qwenvl.txt 仅使用 {model_description}
# main_system.txt / main_system_vl.txt 仅使用 {model_description}
return system_prompt_template.format(
model_description=prompt_replacements.get("model_description", "")
)

View File

@ -125,4 +125,4 @@ class PromptMixin:
if prompt_file.exists():
with open(prompt_file, 'r', encoding='utf-8') as f:
return f.read()
return "你是一个AI助手。"
return "你是 Astrion一个智能助手。"

View File

@ -26,7 +26,6 @@ try:
READONLY_RUN_COMMAND_ALLOWED,
READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS,
READONLY_RUN_COMMAND_BLOCKED_TOKENS,
AGENT_SKILLS_DIR,
)
except ImportError:
import sys
@ -50,7 +49,6 @@ except ImportError:
READONLY_RUN_COMMAND_ALLOWED,
READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS,
READONLY_RUN_COMMAND_BLOCKED_TOKENS,
AGENT_SKILLS_DIR,
)
from modules.file_manager import FileManager
@ -496,9 +494,6 @@ class MainTerminalToolsExecutionMixin:
return (project_root / candidate).resolve()
def _get_create_skill_target_root(self) -> Path:
if self._is_host_mode():
return Path(AGENT_SKILLS_DIR).expanduser().resolve()
inferred = infer_private_skills_dir(self.data_dir)
if inferred:
return inferred
@ -641,9 +636,12 @@ class MainTerminalToolsExecutionMixin:
def _track_shallow_versioning(self, file_path: Any) -> None:
"""Track a file edit for shallow versioning (conversation-scoped undo)."""
conversation_id = getattr(self.context_manager, "current_conversation_id", None)
# 优先使用当前用户消息的 message_id让文件修改归属到正确的输入节点。
message_id = getattr(self.context_manager, "current_shallow_message_id", None) or conversation_id
from utils.perf_log import perf_log
perf_log("_track_shallow_versioning called", extra={
"conversation_id": conversation_id,
"message_id": message_id,
"project_path": str(self.project_path),
"data_dir": str(self.data_dir),
"file_path": str(file_path),
@ -657,9 +655,10 @@ class MainTerminalToolsExecutionMixin:
data_dir=self.data_dir,
conversation_id=conversation_id,
)
manager.track_edit(file_path, conversation_id)
manager.track_edit(file_path, message_id)
perf_log("_track_shallow_versioning done", extra={
"conversation_id": conversation_id,
"message_id": message_id,
"file_path": str(file_path),
"tracked_files_count": len(getattr(manager, "_tracked_files", set())),
})

View File

@ -27,7 +27,7 @@ class AgentSystem:
async def initialize(self):
"""初始化系统"""
print("\n" + "="*50)
print("🤖 AI Agent 系统启动")
print("🤖 Astrion 启动")
print("="*50)
# 1. 获取项目路径

View File

@ -7,17 +7,55 @@ from typing import Dict, List
from config import HOST_SANDBOX_MACOS_WRITABLE_PATHS, deploy_config_path
# macOS 默认拒绝读取的敏感路径(支持 ~/ 前缀)。
DEFAULT_MACOS_DENY_READ_PATHS = [
"~/.ssh",
"~/.aws",
"~/.azure",
"~/.gcp",
"~/.google",
"~/.kube",
"~/.docker",
"~/.gnupg",
"~/.npmrc",
"~/.netrc",
"~/.pypirc",
"~/.git-credentials",
"~/.bash_history",
"~/.zsh_history",
"~/.psql_history",
"~/.mysql_history",
"~/.pgpass",
"/Library/Keychains",
"~/Library/Keychains",
]
# 默认按正则拒绝读取的敏感文件(可匹配文件系统中任意位置)。
DEFAULT_MACOS_DENY_READ_REGEXES = [
r"^/.*\.env(\.[^/]*)?$",
]
_LOCK = threading.Lock()
# 部署级配置(机器特定可读写路径,会被运行时写回)→ ~/.agents/<mode>/config
_POLICY_PATH = Path(deploy_config_path("host_sandbox_policy.json"))
def _default_policy() -> Dict:
return {
"macos_writable_paths": [],
"macos_readable_extra_paths": [],
"macos_deny_read_paths": list(DEFAULT_MACOS_DENY_READ_PATHS),
"macos_deny_read_regexes": list(DEFAULT_MACOS_DENY_READ_REGEXES),
}
def _ensure_file() -> None:
if _POLICY_PATH.exists():
return
_POLICY_PATH.parent.mkdir(parents=True, exist_ok=True)
_POLICY_PATH.write_text(
json.dumps({"macos_writable_paths": [], "macos_readable_extra_paths": []}, ensure_ascii=False, indent=2),
json.dumps(_default_policy(), ensure_ascii=False, indent=2),
encoding="utf-8",
)
@ -28,30 +66,38 @@ def load_policy() -> Dict:
try:
data = json.loads(_POLICY_PATH.read_text(encoding="utf-8"))
except Exception:
data = {"macos_writable_paths": [], "macos_readable_extra_paths": []}
data = _default_policy()
if not isinstance(data, dict):
data = {"macos_writable_paths": [], "macos_readable_extra_paths": []}
writable_items = data.get("macos_writable_paths")
if not isinstance(writable_items, list):
writable_items = []
readable_items = data.get("macos_readable_extra_paths")
if not isinstance(readable_items, list):
readable_items = []
data["macos_writable_paths"] = [str(x).strip() for x in writable_items if str(x).strip()]
data["macos_readable_extra_paths"] = [str(x).strip() for x in readable_items if str(x).strip()]
data = _default_policy()
defaults = _default_policy()
needs_save = False
for key in defaults:
if key not in data or not isinstance(data.get(key), list):
data[key] = list(defaults[key])
needs_save = True
data["macos_writable_paths"] = [str(x).strip() for x in data["macos_writable_paths"] if str(x).strip()]
data["macos_readable_extra_paths"] = [str(x).strip() for x in data["macos_readable_extra_paths"] if str(x).strip()]
data["macos_deny_read_paths"] = [str(x).strip() for x in data["macos_deny_read_paths"] if str(x).strip()]
data["macos_deny_read_regexes"] = [str(x).strip() for x in data["macos_deny_read_regexes"] if str(x).strip()]
if needs_save:
try:
_POLICY_PATH.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
except Exception:
pass
return data
def save_policy(policy: Dict) -> Dict:
payload = dict(policy or {})
writable_items = payload.get("macos_writable_paths")
if not isinstance(writable_items, list):
writable_items = []
readable_items = payload.get("macos_readable_extra_paths")
if not isinstance(readable_items, list):
readable_items = []
payload["macos_writable_paths"] = [str(x).strip() for x in writable_items if str(x).strip()]
payload["macos_readable_extra_paths"] = [str(x).strip() for x in readable_items if str(x).strip()]
defaults = _default_policy()
for key in defaults:
items = payload.get(key)
if not isinstance(items, list):
items = list(defaults[key])
payload[key] = [str(x).strip() for x in items if str(x).strip()]
with _LOCK:
_ensure_file()
_POLICY_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
@ -77,3 +123,25 @@ def get_macos_readable_paths() -> List[str]:
if val and val not in merged:
merged.append(val)
return merged
def get_macos_deny_read_paths() -> List[str]:
data = load_policy()
paths = data.get("macos_deny_read_paths", [])
merged: List[str] = []
for raw in list(DEFAULT_MACOS_DENY_READ_PATHS) + list(paths or []):
val = str(raw).strip()
if val and val not in merged:
merged.append(val)
return merged
def get_macos_deny_read_regexes() -> List[str]:
data = load_policy()
patterns = data.get("macos_deny_read_regexes", [])
merged: List[str] = []
for raw in list(DEFAULT_MACOS_DENY_READ_REGEXES) + list(patterns or []):
val = str(raw).strip()
if val and val not in merged:
merged.append(val)
return merged

View File

@ -7,7 +7,11 @@ import shlex
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional
from modules.host_sandbox_policy import get_macos_writable_paths
from modules.host_sandbox_policy import (
get_macos_writable_paths,
get_macos_deny_read_paths,
get_macos_deny_read_regexes,
)
@dataclass
@ -22,6 +26,84 @@ class HostSandboxError(RuntimeError):
pass
# macOS 最小可读系统路径集合Codex 风格 deny-default + allow-list
# 当前仅作为常量保留,供后续需要严格 allow-list 的只读沙箱模式使用。
# 当前只读沙箱采用“全局可读 + 敏感路径拒绝”模型,以保证工具兼容性。
MACOS_MINIMAL_READABLE_PATHS = [
"/bin",
"/sbin",
"/usr/bin",
"/usr/sbin",
"/usr/libexec",
"/usr/lib",
"/usr/share",
"/usr/local/lib",
"/opt/homebrew/lib",
"/lib",
"/etc",
"/private/etc",
"/tmp",
"/private/tmp",
"/var/tmp",
"/private/var/tmp",
"/var",
"/private/var",
"/dev",
"/System/Library/Frameworks",
"/System/Library/PrivateFrameworks",
"/System/Library/SubFrameworks",
"/System/Library/CoreServices",
"/System/Library/Extensions",
"/Library/Apple",
"/Library/Preferences",
"/Library/Filesystems/NetFSPlugins",
"/Applications",
]
def _expand_path(raw: str) -> Optional[str]:
"""展开路径中的 ~ 并返回绝对路径;无法展开时返回 None。"""
if not raw:
return None
try:
expanded = str(Path(raw).expanduser().resolve())
except Exception:
return None
return expanded
def _build_macos_read_rules(paths: List[str]) -> str:
"""把路径列表转成 (allow file-read* (subpath ...)) 规则段。"""
rules: List[str] = []
seen: set[str] = set()
for raw in paths:
expanded = _expand_path(raw)
if expanded and expanded not in seen:
seen.add(expanded)
rules.append(f'(allow file-read* (subpath "{expanded}"))')
return "\n".join(rules)
def _build_macos_deny_rules(paths: List[str]) -> str:
"""把路径列表转成 (deny file-read* (subpath ...)) 规则段。"""
rules: List[str] = []
seen: set[str] = set()
for raw in paths:
expanded = _expand_path(raw)
if expanded and expanded not in seen:
seen.add(expanded)
rules.append(f'(deny file-read* (subpath "{expanded}"))')
return "\n".join(rules)
def _build_macos_deny_regex_rules(patterns: List[str]) -> str:
"""把正则列表转成 (deny file-read* (regex #"...")) 规则段。"""
rules: List[str] = []
for pattern in patterns:
rules.append(f'(deny file-read* (regex #"{pattern}"))')
return "\n".join(rules)
# 宿主机网络权限档位
NETWORK_PERMISSION_RESTRICTED = "restricted" # macOS: 仅本地回环Linux/Windows: 暂不隔离
NETWORK_PERMISSION_FULL = "full" # 完全开放
@ -131,13 +213,24 @@ def _build_macos_readonly_plan(
if not sandbox_exec:
raise HostSandboxError("macOS 未找到 sandbox-exec拒绝执行宿主机命令。")
network_policy = _build_macos_network_policy(network_permission)
workspace = str(work_path.resolve())
# 只读沙箱:全局可读 + 敏感路径/文件拒绝,写权限仅 /dev/null。
# 工作区在 deny 规则之后再显式 allow保证“工作区内不受沙箱影响”。
deny_rules = _build_macos_deny_rules(get_macos_deny_read_paths())
regex_rules = _build_macos_deny_regex_rules(get_macos_deny_read_regexes())
if regex_rules:
deny_rules += "\n" + regex_rules
profile = (
'(version 1) '
'(deny default) '
'(allow sysctl-read) '
'(allow process*) '
'(version 1)\n'
'(deny default)\n'
'(allow sysctl-read)\n'
'(allow process*)\n'
f'{network_policy}'
'(allow file-read*) '
'(allow file-read*)\n'
f'{deny_rules}\n'
f'(allow file-read* (subpath "{workspace}"))\n'
'(allow file-write* (literal "/dev/null"))'
)
cmd = [sandbox_exec, "-p", profile, "/bin/bash", "-lc", command]
@ -178,13 +271,20 @@ def _macos_profile_for_workspace(
write_rules.append(f'(subpath "{entry}")')
write_expr = " ".join(write_rules)
network_policy = _build_macos_network_policy(network_permission)
workspace = str(work_path.resolve())
deny_rules = _build_macos_deny_rules(get_macos_deny_read_paths())
regex_rules = _build_macos_deny_regex_rules(get_macos_deny_read_regexes())
if regex_rules:
deny_rules += "\n" + regex_rules
return (
'(version 1) '
'(deny default) '
'(allow sysctl-read) '
'(allow process*) '
'(version 1)\n'
'(deny default)\n'
'(allow sysctl-read)\n'
'(allow process*)\n'
f'{network_policy}'
'(allow file-read*) '
'(allow file-read*)\n'
f'{deny_rules}\n'
f'(allow file-read* (subpath "{workspace}"))\n'
f'(allow file-write* {write_expr})'
)

View File

@ -189,6 +189,14 @@ class ShallowVersioningManager:
# ----------------------------
# public API
# ----------------------------
def _find_latest_snapshot_by_message_id(self, message_id: str) -> Optional[ShallowSnapshot]:
"""Return the most recent snapshot whose message_id matches."""
target = str(message_id or "")
for snapshot in reversed(self._snapshots):
if str(snapshot.message_id or "") == target:
return snapshot
return None
def track_edit(self, file_path: Path | str, message_id: str) -> None:
"""Track a file before it is edited; backup its current content if not already tracked."""
tracking_path = self._normalize_path(file_path)
@ -206,12 +214,13 @@ class ShallowVersioningManager:
backup = self._create_backup(resolved, 1)
self._tracked_files.add(tracking_path)
# Add to the most recent snapshot if one exists, otherwise create a pre-edit snapshot.
# 归属到当前 message_id 对应的最新 snapshot不存在则新建。
# 避免把本次输入的修改追加到上一个输入的 snapshot。
now = datetime.now().isoformat()
if self._snapshots:
latest = self._snapshots[-1]
latest.tracked_file_backups[tracking_path] = backup
self._persist_snapshot(latest)
target_snapshot = self._find_latest_snapshot_by_message_id(message_id)
if target_snapshot is not None:
target_snapshot.tracked_file_backups[tracking_path] = backup
self._persist_snapshot(target_snapshot)
else:
snapshot = ShallowSnapshot(
message_id=message_id,
@ -256,8 +265,17 @@ class ShallowVersioningManager:
tracked_file_backups=tracked_file_backups,
timestamp=now,
)
self._snapshots.append(snapshot)
self._snapshot_sequence += 1
# 如果当前 message_id 已存在 snapshot由 track_edit 预创建),则替换它,
# 避免同一个输入节点留下多个 snapshot 导致 diff 只计算最后两次之间的变化。
existing_index = -1
for idx, s in enumerate(self._snapshots):
if str(s.message_id or "") == str(message_id or ""):
existing_index = idx
if existing_index >= 0:
self._snapshots[existing_index] = snapshot
else:
self._snapshots.append(snapshot)
self._snapshot_sequence += 1
self._persist_snapshot(snapshot)
# Evict old snapshots while keeping the most recent MAX_SNAPSHOTS.

View File

@ -11,7 +11,7 @@ import threading
from pathlib import Path
from typing import Dict, List, Optional, Sequence
from config import AGENT_SKILLS_DIR, WORKSPACE_SKILLS_DIRNAME
from config import AGENT_SKILLS_DIR, CUSTOM_SKILLS_DIR, IS_HOST_MODE, WORKSPACE_SKILLS_DIRNAME
from utils.logger import setup_logger
logger = setup_logger(__name__)
@ -48,10 +48,15 @@ def ensure_workspace_skills_dir(project_path: str | Path) -> Path:
def infer_private_skills_dir(data_dir: str | Path | None) -> Optional[Path]:
"""Infer per-user private skills dir from workspace data_dir."""
"""Infer per-user private skills dir from workspace data_dir.
host 模式下统一使用运行态根下的 ``agentskills``不再按用户拆分
"""
if not data_dir:
return None
try:
if IS_HOST_MODE:
return Path(CUSTOM_SKILLS_DIR).expanduser().resolve()
data_path = Path(data_dir).expanduser().resolve()
if data_path.name == "data" and data_path.parent.parent.name == "workspaces":
return (data_path.parent.parent.parent / "agentskills").resolve()

View File

@ -1,5 +1,5 @@
{
"name": "ai-agent-frontend",
"name": "astrion-frontend",
"version": "4.1.0",
"private": true,
"type": "module",

View File

@ -1,6 +1,8 @@
你是 Astrion一个运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。
{model_description}
你是一名运行在云端服务器/用户电脑(根据工作区信息判断)根据上的全能型智能助手,专为**普通用户和开发者**设计。你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
- **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤
- **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议

View File

@ -1,6 +1,8 @@
你是 Astrion一个运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。
{model_description}
你是一名运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
- **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤
- **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议

View File

@ -207,7 +207,7 @@ def _write_custom_models(target_path: Path, entry: dict) -> Path | None:
def run_wizard(force: bool = False) -> int:
print("=" * 60)
print(" AI Agent 首次启动初始化向导")
print(" Astrion 首次启动初始化向导")
print("=" * 60)
print("逐项设置启动前必须确定的内容,每项可回车使用 [默认值]。")
print()
@ -311,7 +311,7 @@ def run_wizard(force: bool = False) -> int:
def main() -> int:
parser = argparse.ArgumentParser(description="AI Agent 首次启动初始化向导")
parser = argparse.ArgumentParser(description="Astrion 首次启动初始化向导")
parser.add_argument(
"--force",
action="store_true",

View File

@ -35,7 +35,12 @@ from modules.skills_manager import (
sync_workspace_skills,
)
from modules.upload_security import UploadSecurityError
from modules.host_sandbox_policy import load_policy, save_policy
from modules.host_sandbox_policy import (
load_policy,
save_policy,
get_macos_deny_read_paths,
get_macos_deny_read_regexes,
)
from modules.user_manager import UserWorkspace
from core.web_terminal import WebTerminal
from config.model_profiles import get_model_context_window
@ -49,8 +54,30 @@ from server.state import tool_approval_manager, user_question_manager
from server.extensions import socketio
from server.monitor import get_cached_monitor_snapshot
from server.files import sanitize_filename_preserve_unicode
import os
import re
UPLOAD_FOLDER_NAME = ".agents/user_upload"
def _path_conflicts_with_deny_list(path: str) -> Optional[str]:
"""检查用户授权路径是否与内置 deny 列表冲突,返回错误信息或 None。"""
if not path:
return None
expanded = os.path.abspath(os.path.expanduser(path))
for deny_path in get_macos_deny_read_paths():
deny_expanded = os.path.abspath(os.path.expanduser(deny_path))
deny_lower = deny_expanded.lower().rstrip("/")
expanded_lower = expanded.lower().rstrip("/")
if expanded_lower == deny_lower or expanded_lower.startswith(deny_lower + "/"):
return f"禁止授权敏感路径: {path}"
for pattern in get_macos_deny_read_regexes():
try:
if re.search(pattern, expanded) or re.search(pattern, path):
return f"禁止授权敏感文件: {path}"
except re.error:
continue
return None
@chat_bp.route('/api/permission-mode', methods=['GET'])
@api_login_required
@with_terminal
@ -337,6 +364,8 @@ def get_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, user
"enabled": can_manage,
"writable_paths": data.get("macos_writable_paths", []),
"readable_extra_paths": data.get("macos_readable_extra_paths", []),
"deny_read_paths": data.get("macos_deny_read_paths", []),
"deny_read_regexes": data.get("macos_deny_read_regexes", []),
})
@chat_bp.route('/api/path-authorization', methods=['POST'])
@ -357,11 +386,10 @@ def update_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, u
readable_extra = [str(x).strip() for x in readable_items if str(x).strip()]
if "/" in writable or "/" in readable_extra:
return jsonify({"success": False, "error": "禁止授权根目录 /"}), 400
blocked_prefix = ("~/.ssh", "~/.gnupg", "~/.aws")
for p in writable + readable_extra:
lowered = p.lower()
if any(lowered.startswith(prefix) for prefix in blocked_prefix):
return jsonify({"success": False, "error": f"禁止授权敏感目录: {p}"}), 400
conflict = _path_conflicts_with_deny_list(p)
if conflict:
return jsonify({"success": False, "error": conflict}), 400
payload = save_policy({
"macos_writable_paths": writable,
"macos_readable_extra_paths": readable_extra
@ -370,4 +398,6 @@ def update_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, u
"success": True,
"writable_paths": payload.get("macos_writable_paths", []),
"readable_extra_paths": payload.get("macos_readable_extra_paths", []),
"deny_read_paths": payload.get("macos_deny_read_paths", []),
"deny_read_regexes": payload.get("macos_deny_read_regexes", []),
})

View File

@ -431,6 +431,14 @@ def _record_hidden_versioning_checkpoint_after_run(
debug_log(f"[Versioning] 记录快照失败: {exc}")
except Exception as exc:
debug_log(f"[Versioning] 记录快照异常: {exc}")
finally:
# 当前输入处理结束,清除浅备份消息 ID避免后续无消息上下文时的错误归属。
try:
ctx = getattr(web_terminal, "context_manager", None)
if ctx is not None and hasattr(ctx, "current_shallow_message_id"):
ctx.current_shallow_message_id = None
except Exception:
pass
def _persist_and_echo_preceding_notice(
@ -1010,6 +1018,13 @@ async def handle_task_with_sender(
videos=videos,
metadata=user_message_metadata
)
# 为浅备份 track_edit 提供当前用户消息 ID避免文件修改被归到上一个输入。
try:
web_terminal.context_manager.current_shallow_message_id = (
saved_user_message.get("message_id") if isinstance(saved_user_message, dict) else None
)
except Exception:
web_terminal.context_manager.current_shallow_message_id = None
try:
user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1
except Exception:

View File

@ -1074,14 +1074,16 @@ def get_conversation_versioning_checkpoint_detail(conversation_id, seq: int, ter
shallow_message_id = snapshot_data.get("shallow_message_id")
except Exception:
pass
files = row.get("files") or []
debug_log(f"[Versioning][Detail] resolved shallow_message_id={shallow_message_id} files={files}")
if shallow_message_id and isinstance(files, list):
debug_log(f"[Versioning][Detail] resolved shallow_message_id={shallow_message_id}")
if shallow_message_id:
shallow_manager = ShallowVersioningManager(
project_path=workspace.project_path,
data_dir=workspace.data_dir,
conversation_id=normalized_id,
)
# 与列表接口保持一致:从 shallow manager 重新计算文件变更,
# 而不是只给 inputs.jsonl 中原有的 files 补 patch_lines。
files = shallow_manager.get_file_diff_stats(str(shallow_message_id)) or []
stats = shallow_manager.get_diff_stats(str(shallow_message_id)) or {}
normalized_files: List[Dict[str, Any]] = []
for file_item in files:
@ -1091,7 +1093,7 @@ def get_conversation_versioning_checkpoint_detail(conversation_id, seq: int, ter
path = str(item.get("path") or "")
if path:
patch = shallow_manager.get_file_patch_lines(path, str(shallow_message_id))
debug_log(f"[Versioning][Detail] path={path} patch={patch}")
debug_log(f"[Versioning][Detail] path={path} patch_lines={len(patch.get('lines') or [])}")
item["patch_lines"] = patch.get("lines") or []
item["patch_truncated"] = bool(patch.get("truncated"))
normalized_files.append(item)

View File

@ -30,7 +30,7 @@
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
<span>AI Assistant</span>
<span>Astrion</span>
</div>
<!-- 等待动画 -->

View File

@ -30,7 +30,7 @@
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
<span>AI Assistant</span>
<span>Astrion</span>
</div>
<!-- 等待动画 -->

View File

@ -68,7 +68,7 @@
</head>
<body>
<div class="sidebar">
<div class="logo">AI Agent <span>v5.3</span></div>
<div class="logo">Astrion <span>v5.3</span></div>
<div class="nav-item active">📁 项目文件</div>
<div class="nav-item">⚙️ 管理面板</div>
<div style="margin-top:20px; font-size:12px; color:#9CA3AF; font-weight:600;">TEMP</div>

View File

@ -133,7 +133,7 @@
</head>
<body>
<div class="sidebar">
<div class="logo">AI Agent</div>
<div class="logo">Astrion</div>
<div class="nav-item active">Project Files</div>
<div class="nav-item">Dashboard</div>
<div style="flex:1"></div>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Agent System</title>
<title>Astrion</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/easter-eggs/flood.css">
<link rel="stylesheet" href="/static/easter-eggs/snake.css">

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录 - AI Agent</title>
<title>登录 - Astrion</title>
<script>
(function () {
try {

View File

@ -255,7 +255,7 @@
</div>
<div class="sheet-body">
<div class="section-card status-card">
<strong>AI Agent v2.4</strong>
<strong>Astrion v2.4</strong>
<p>已连接 · 思考模式</p>
</div>
<div class="section-card">

View File

@ -335,7 +335,7 @@
</div>
<div class="sheet-body">
<div class="section-card">
<strong>AI Agent v2.4</strong>
<strong>Astrion v2.4</strong>
<p>思考模式 · 已连接</p>
</div>
<div class="section-card">

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>注册 - AI Agent</title>
<title>注册 - Astrion</title>
<script>
(function () {
try {

View File

@ -1,7 +1,7 @@
<template>
<main class="auth-page">
<section class="auth-card">
<h1 class="auth-title">AI Agent 登录</h1>
<h1 class="auth-title">Astrion 登录</h1>
<p class="auth-subtitle">使用账号继续访问工作台</p>
<div class="auth-form-group">

View File

@ -98,7 +98,7 @@
</template>
</div>
<div v-else-if="msg.role === 'assistant'" class="assistant-message">
<!-- assistant 消息前显示 AI Assistant 头部 -->
<!-- assistant 消息前显示 Astrion 头部 -->
<!-- 只有当前一条消息是 user 且当前消息有内容时才显示 -->
<div
v-if="
@ -704,9 +704,9 @@ const stackedBlocksEnabled = computed(() => {
});
const aiAssistantName = computed(() => {
if (!personalization.form.use_custom_names) {
return 'AI Assistant';
return 'Astrion';
}
return personalization.form.self_identify || 'AI Assistant';
return personalization.form.self_identify || 'Astrion';
});
const userName = computed(() => {
if (!personalization.form.use_custom_names) {

View File

@ -24,11 +24,22 @@
仅可读
</button>
</div>
<textarea
<p class="hint">
{{
mode === 'writable'
? '可读可写路径在 workspace-write 沙箱中可写入,只读沙箱中仅可读。'
: '仅可读路径在只读沙箱中会被加入允许读取列表;工作区可写沙箱默认已可读。'
}}
</p>
<textarea
class="path-input"
:value="value"
@input="$emit('update:value', ($event.target as HTMLTextAreaElement).value)"
placeholder="每行一个路径,例如:~/Desktop/agents-export"
:placeholder="
mode === 'writable'
? '每行一个路径,例如:~/Desktop/agents-export'
: '每行一个路径,例如:~/Documents/reference'
"
/>
<div class="actions">
<button type="button" class="btn" @click="$emit('save')" :disabled="saving">保存</button>

View File

@ -10,7 +10,7 @@
aria-hidden="true"
></span>
<div class="brand-text">
<span class="brand-name">AI Agent</span>
<span class="brand-name">Astrion</span>
<span class="agent-version" v-if="agentVersion">{{ agentVersion }}</span>
</div>
</div>

View File

@ -28,7 +28,7 @@ const TUTORIAL_VERSION = 'v1';
const DEFAULT_STEPS: TutorialStep[] = [
{
id: 'welcome',
title: '欢迎使用 AI Agent 系统',
title: '欢迎使用 Astrion',
description: '我们将用 2-3 分钟带你快速了解核心功能。',
target: null,
mode: 'info',

View File

@ -402,7 +402,7 @@
</div>
</div>
<div class="status-right">
<span style="opacity: 0.6;">AI Agent Terminal Monitor v1.0</span>
<span style="opacity: 0.6;">Astrion Terminal Monitor v1.0</span>
</div>
</div>
@ -555,7 +555,7 @@
term.writeln('\x1b[1;36m║ 🤖 AI Terminal Monitor v1.0 ║\x1b[0m');
term.writeln('\x1b[1;36m╚════════════════════════════════════════╝\x1b[0m');
term.writeln('');
term.writeln('\x1b[33m正在连接到AI Agent...\x1b[0m');
term.writeln('\x1b[33m正在连接到 Astrion...\x1b[0m');
term.writeln('');
}