Compare commits
No commits in common. "6a2cb820bcd5c81585e3227e7eb2624c16637642" and "2264a86851df6fbbfcdb7702992e47ee6763216a" have entirely different histories.
6a2cb820bc
...
2264a86851
@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
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`
|
|
||||||
10
AGENTS.md
10
AGENTS.md
@ -7,16 +7,6 @@
|
|||||||
|
|
||||||
> 和用户交流时默认使用中文。
|
> 和用户交流时默认使用中文。
|
||||||
|
|
||||||
## 0) 项目名称
|
|
||||||
|
|
||||||
本项目(仓库 `agents`)的暂定产品名为 **Astrion**。
|
|
||||||
|
|
||||||
- **构词**:`Astr-`(希腊语 *astron*,星星/天体)+ `-ion`(粒子/状态后缀,如 photon、electron)。
|
|
||||||
- **意象**:星之子、星际粒子、来自宇宙尘埃的微小发光体。
|
|
||||||
- **寓意**:Agent 像星际间传递信号的粒子,把人类意图传递到工具、文件、终端与子智能体之间;同时把零散任务像星尘聚合成恒星一样,聚合成可执行结果。
|
|
||||||
- **状态**:暂定名,后续若变更需同步更新 `AGENTS.md`、`claude.md` 与项目记忆 `astrion_naming`。
|
|
||||||
- **中文参考**:阿斯特里恩;简称可考虑 **星子**。
|
|
||||||
|
|
||||||
## 1) 当前项目结构(按代码现状)
|
## 1) 当前项目结构(按代码现状)
|
||||||
|
|
||||||
- **后端入口**
|
- **后端入口**
|
||||||
|
|||||||
@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
多用户 AI Agent 系统,产品暂定名为 **Astrion**(`Astr-` 星星/天体 + `-ion` 粒子后缀,意象为“星之子、星际粒子”)。为每个登录用户提供独立的 Docker 容器环境,支持终端交互、文件操作、对话管理和实时监控。主要用于学习和实验"智能体 + 真实 Dev Workflow",代码大量由 AI 生成。
|
多用户 AI Agent 系统,为每个登录用户提供独立的 Docker 容器环境。支持终端交互、文件操作、对话管理和实时监控。主要用于学习和实验"智能体 + 真实 Dev Workflow",代码大量由 AI 生成。
|
||||||
|
|
||||||
## Core Architecture
|
## Core Architecture
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Astrion
|
# AI Agent 系统
|
||||||
|
|
||||||
一个功能完整的 AI 智能体系统,支持多模态交互、子智能体协作、实时终端操作和丰富的工具集成。
|
一个功能完整的 AI 智能体系统,支持多模态交互、子智能体协作、实时终端操作和丰富的工具集成。
|
||||||
|
|
||||||
|
|||||||
15
agentskills/test/SKILL.md
Normal file
15
agentskills/test/SKILL.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@ -97,7 +97,7 @@ function isWideCodePoint(code: number): boolean {
|
|||||||
export function WelcomePanel({ status }: { status: CliStatus }) {
|
export function WelcomePanel({ status }: { status: CliStatus }) {
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginTop={1}>
|
<Box flexDirection="column" marginTop={1}>
|
||||||
<Text color="gray">Astrion</Text>
|
<Text color="gray">Agents</Text>
|
||||||
<Box borderStyle="round" borderColor="gray" paddingX={2} paddingY={1} flexDirection="column">
|
<Box borderStyle="round" borderColor="gray" paddingX={2} paddingY={1} flexDirection="column">
|
||||||
<Text bold>Welcome back!</Text>
|
<Text bold>Welcome back!</Text>
|
||||||
<Text color="gray">{status.model} · {status.runMode}</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 }) {
|
export function StatusPanel({ status }: { status: CliStatus }) {
|
||||||
return (
|
return (
|
||||||
<Box borderStyle="round" borderColor="gray" paddingX={1} flexDirection="column" marginTop={1}>
|
<Box borderStyle="round" borderColor="gray" paddingX={1} flexDirection="column" marginTop={1}>
|
||||||
<Text>{'>_ Astrion CLI'}</Text>
|
<Text>{'>_ Agents CLI'}</Text>
|
||||||
<Text> </Text>
|
<Text> </Text>
|
||||||
<StatusField label="Model" value={status.model} />
|
<StatusField label="Model" value={status.model} />
|
||||||
<StatusField label="Directory" value={shortPath(status.directory)} />
|
<StatusField label="Directory" value={shortPath(status.directory)} />
|
||||||
|
|||||||
@ -1,28 +1,4 @@
|
|||||||
{
|
{
|
||||||
"macos_writable_paths": [],
|
"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(\\.[^/]*)?$"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -105,9 +105,6 @@ USER_SPACE_DIR = _runtime_dir("USER_SPACE_DIR", "users")
|
|||||||
# API 专用用户与工作区(与网页用户隔离)
|
# API 专用用户与工作区(与网页用户隔离)
|
||||||
API_USER_SPACE_DIR = _runtime_dir("API_USER_SPACE_DIR", "api/users")
|
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"
|
IS_HOST_MODE = _MODE == "host"
|
||||||
|
|
||||||
@ -193,7 +190,6 @@ __all__ = [
|
|||||||
"INVITE_CODES_FILE",
|
"INVITE_CODES_FILE",
|
||||||
"ADMIN_POLICY_FILE",
|
"ADMIN_POLICY_FILE",
|
||||||
"API_USER_SPACE_DIR",
|
"API_USER_SPACE_DIR",
|
||||||
"CUSTOM_SKILLS_DIR",
|
|
||||||
"API_USERS_DB_FILE",
|
"API_USERS_DB_FILE",
|
||||||
"API_TOKENS_FILE",
|
"API_TOKENS_FILE",
|
||||||
"API_USAGE_FILE",
|
"API_USAGE_FILE",
|
||||||
|
|||||||
@ -112,13 +112,13 @@ class MessagesMixin:
|
|||||||
# 根据当前模型多模态能力选择系统提示。system prompt 按对话冻结;
|
# 根据当前模型多模态能力选择系统提示。system prompt 按对话冻结;
|
||||||
# 工具 schema 保持动态,由 define_tools 每轮根据真实状态生成。
|
# 工具 schema 保持动态,由 define_tools 每轮根据真实状态生成。
|
||||||
current_model = getattr(self, "model_key", None)
|
current_model = getattr(self, "model_key", None)
|
||||||
prompt_name = "main_system_vl" if (model_supports_image(current_model) or model_supports_video(current_model)) else "main_system"
|
prompt_name = "main_system_qwenvl" if (model_supports_image(current_model) or model_supports_video(current_model)) else "main_system"
|
||||||
model_key = getattr(self, "model_key", None)
|
model_key = getattr(self, "model_key", None)
|
||||||
prompt_replacements = get_model_prompt_replacements(model_key)
|
prompt_replacements = get_model_prompt_replacements(model_key)
|
||||||
|
|
||||||
def _build_main_system_prompt() -> str:
|
def _build_main_system_prompt() -> str:
|
||||||
system_prompt_template = self.load_prompt(prompt_name)
|
system_prompt_template = self.load_prompt(prompt_name)
|
||||||
# main_system.txt / main_system_vl.txt 仅使用 {model_description}
|
# main_system.txt / main_system_qwenvl.txt 仅使用 {model_description}
|
||||||
return system_prompt_template.format(
|
return system_prompt_template.format(
|
||||||
model_description=prompt_replacements.get("model_description", "")
|
model_description=prompt_replacements.get("model_description", "")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -125,4 +125,4 @@ class PromptMixin:
|
|||||||
if prompt_file.exists():
|
if prompt_file.exists():
|
||||||
with open(prompt_file, 'r', encoding='utf-8') as f:
|
with open(prompt_file, 'r', encoding='utf-8') as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
return "你是 Astrion,一个智能助手。"
|
return "你是一个AI助手。"
|
||||||
|
|||||||
@ -26,6 +26,7 @@ try:
|
|||||||
READONLY_RUN_COMMAND_ALLOWED,
|
READONLY_RUN_COMMAND_ALLOWED,
|
||||||
READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS,
|
READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS,
|
||||||
READONLY_RUN_COMMAND_BLOCKED_TOKENS,
|
READONLY_RUN_COMMAND_BLOCKED_TOKENS,
|
||||||
|
AGENT_SKILLS_DIR,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import sys
|
import sys
|
||||||
@ -49,6 +50,7 @@ except ImportError:
|
|||||||
READONLY_RUN_COMMAND_ALLOWED,
|
READONLY_RUN_COMMAND_ALLOWED,
|
||||||
READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS,
|
READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS,
|
||||||
READONLY_RUN_COMMAND_BLOCKED_TOKENS,
|
READONLY_RUN_COMMAND_BLOCKED_TOKENS,
|
||||||
|
AGENT_SKILLS_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
from modules.file_manager import FileManager
|
from modules.file_manager import FileManager
|
||||||
@ -494,6 +496,9 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
return (project_root / candidate).resolve()
|
return (project_root / candidate).resolve()
|
||||||
|
|
||||||
def _get_create_skill_target_root(self) -> Path:
|
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)
|
inferred = infer_private_skills_dir(self.data_dir)
|
||||||
if inferred:
|
if inferred:
|
||||||
return inferred
|
return inferred
|
||||||
@ -636,12 +641,9 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
def _track_shallow_versioning(self, file_path: Any) -> None:
|
def _track_shallow_versioning(self, file_path: Any) -> None:
|
||||||
"""Track a file edit for shallow versioning (conversation-scoped undo)."""
|
"""Track a file edit for shallow versioning (conversation-scoped undo)."""
|
||||||
conversation_id = getattr(self.context_manager, "current_conversation_id", None)
|
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
|
from utils.perf_log import perf_log
|
||||||
perf_log("_track_shallow_versioning called", extra={
|
perf_log("_track_shallow_versioning called", extra={
|
||||||
"conversation_id": conversation_id,
|
"conversation_id": conversation_id,
|
||||||
"message_id": message_id,
|
|
||||||
"project_path": str(self.project_path),
|
"project_path": str(self.project_path),
|
||||||
"data_dir": str(self.data_dir),
|
"data_dir": str(self.data_dir),
|
||||||
"file_path": str(file_path),
|
"file_path": str(file_path),
|
||||||
@ -655,10 +657,9 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
data_dir=self.data_dir,
|
data_dir=self.data_dir,
|
||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
)
|
)
|
||||||
manager.track_edit(file_path, message_id)
|
manager.track_edit(file_path, conversation_id)
|
||||||
perf_log("_track_shallow_versioning done", extra={
|
perf_log("_track_shallow_versioning done", extra={
|
||||||
"conversation_id": conversation_id,
|
"conversation_id": conversation_id,
|
||||||
"message_id": message_id,
|
|
||||||
"file_path": str(file_path),
|
"file_path": str(file_path),
|
||||||
"tracked_files_count": len(getattr(manager, "_tracked_files", set())),
|
"tracked_files_count": len(getattr(manager, "_tracked_files", set())),
|
||||||
})
|
})
|
||||||
|
|||||||
2
main.py
2
main.py
@ -27,7 +27,7 @@ class AgentSystem:
|
|||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
"""初始化系统"""
|
"""初始化系统"""
|
||||||
print("\n" + "="*50)
|
print("\n" + "="*50)
|
||||||
print("ð¤ Astrion 启动")
|
print("ð¤ AI Agent 系统启动")
|
||||||
print("="*50)
|
print("="*50)
|
||||||
|
|
||||||
# 1. 获取项目路径
|
# 1. 获取项目路径
|
||||||
|
|||||||
@ -7,55 +7,17 @@ from typing import Dict, List
|
|||||||
|
|
||||||
from config import HOST_SANDBOX_MACOS_WRITABLE_PATHS, deploy_config_path
|
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()
|
_LOCK = threading.Lock()
|
||||||
# 部署级配置(机器特定可读写路径,会被运行时写回)→ ~/.agents/<mode>/config
|
# 部署级配置(机器特定可读写路径,会被运行时写回)→ ~/.agents/<mode>/config
|
||||||
_POLICY_PATH = Path(deploy_config_path("host_sandbox_policy.json"))
|
_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:
|
def _ensure_file() -> None:
|
||||||
if _POLICY_PATH.exists():
|
if _POLICY_PATH.exists():
|
||||||
return
|
return
|
||||||
_POLICY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
_POLICY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
_POLICY_PATH.write_text(
|
_POLICY_PATH.write_text(
|
||||||
json.dumps(_default_policy(), ensure_ascii=False, indent=2),
|
json.dumps({"macos_writable_paths": [], "macos_readable_extra_paths": []}, ensure_ascii=False, indent=2),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -66,38 +28,30 @@ def load_policy() -> Dict:
|
|||||||
try:
|
try:
|
||||||
data = json.loads(_POLICY_PATH.read_text(encoding="utf-8"))
|
data = json.loads(_POLICY_PATH.read_text(encoding="utf-8"))
|
||||||
except Exception:
|
except Exception:
|
||||||
data = _default_policy()
|
data = {"macos_writable_paths": [], "macos_readable_extra_paths": []}
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
data = _default_policy()
|
data = {"macos_writable_paths": [], "macos_readable_extra_paths": []}
|
||||||
|
writable_items = data.get("macos_writable_paths")
|
||||||
defaults = _default_policy()
|
if not isinstance(writable_items, list):
|
||||||
needs_save = False
|
writable_items = []
|
||||||
for key in defaults:
|
readable_items = data.get("macos_readable_extra_paths")
|
||||||
if key not in data or not isinstance(data.get(key), list):
|
if not isinstance(readable_items, list):
|
||||||
data[key] = list(defaults[key])
|
readable_items = []
|
||||||
needs_save = True
|
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["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
|
return data
|
||||||
|
|
||||||
|
|
||||||
def save_policy(policy: Dict) -> Dict:
|
def save_policy(policy: Dict) -> Dict:
|
||||||
payload = dict(policy or {})
|
payload = dict(policy or {})
|
||||||
defaults = _default_policy()
|
writable_items = payload.get("macos_writable_paths")
|
||||||
for key in defaults:
|
if not isinstance(writable_items, list):
|
||||||
items = payload.get(key)
|
writable_items = []
|
||||||
if not isinstance(items, list):
|
readable_items = payload.get("macos_readable_extra_paths")
|
||||||
items = list(defaults[key])
|
if not isinstance(readable_items, list):
|
||||||
payload[key] = [str(x).strip() for x in items if str(x).strip()]
|
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()]
|
||||||
with _LOCK:
|
with _LOCK:
|
||||||
_ensure_file()
|
_ensure_file()
|
||||||
_POLICY_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
_POLICY_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
@ -123,25 +77,3 @@ def get_macos_readable_paths() -> List[str]:
|
|||||||
if val and val not in merged:
|
if val and val not in merged:
|
||||||
merged.append(val)
|
merged.append(val)
|
||||||
return merged
|
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
|
|
||||||
|
|||||||
@ -7,11 +7,7 @@ import shlex
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
from modules.host_sandbox_policy import (
|
from modules.host_sandbox_policy import get_macos_writable_paths
|
||||||
get_macos_writable_paths,
|
|
||||||
get_macos_deny_read_paths,
|
|
||||||
get_macos_deny_read_regexes,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -26,84 +22,6 @@ class HostSandboxError(RuntimeError):
|
|||||||
pass
|
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_RESTRICTED = "restricted" # macOS: 仅本地回环;Linux/Windows: 暂不隔离
|
||||||
NETWORK_PERMISSION_FULL = "full" # 完全开放
|
NETWORK_PERMISSION_FULL = "full" # 完全开放
|
||||||
@ -213,24 +131,13 @@ def _build_macos_readonly_plan(
|
|||||||
if not sandbox_exec:
|
if not sandbox_exec:
|
||||||
raise HostSandboxError("macOS 未找到 sandbox-exec,拒绝执行宿主机命令。")
|
raise HostSandboxError("macOS 未找到 sandbox-exec,拒绝执行宿主机命令。")
|
||||||
network_policy = _build_macos_network_policy(network_permission)
|
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 = (
|
profile = (
|
||||||
'(version 1)\n'
|
'(version 1) '
|
||||||
'(deny default)\n'
|
'(deny default) '
|
||||||
'(allow sysctl-read)\n'
|
'(allow sysctl-read) '
|
||||||
'(allow process*)\n'
|
'(allow process*) '
|
||||||
f'{network_policy}'
|
f'{network_policy}'
|
||||||
'(allow file-read*)\n'
|
'(allow file-read*) '
|
||||||
f'{deny_rules}\n'
|
|
||||||
f'(allow file-read* (subpath "{workspace}"))\n'
|
|
||||||
'(allow file-write* (literal "/dev/null"))'
|
'(allow file-write* (literal "/dev/null"))'
|
||||||
)
|
)
|
||||||
cmd = [sandbox_exec, "-p", profile, "/bin/bash", "-lc", command]
|
cmd = [sandbox_exec, "-p", profile, "/bin/bash", "-lc", command]
|
||||||
@ -271,20 +178,13 @@ def _macos_profile_for_workspace(
|
|||||||
write_rules.append(f'(subpath "{entry}")')
|
write_rules.append(f'(subpath "{entry}")')
|
||||||
write_expr = " ".join(write_rules)
|
write_expr = " ".join(write_rules)
|
||||||
network_policy = _build_macos_network_policy(network_permission)
|
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 (
|
return (
|
||||||
'(version 1)\n'
|
'(version 1) '
|
||||||
'(deny default)\n'
|
'(deny default) '
|
||||||
'(allow sysctl-read)\n'
|
'(allow sysctl-read) '
|
||||||
'(allow process*)\n'
|
'(allow process*) '
|
||||||
f'{network_policy}'
|
f'{network_policy}'
|
||||||
'(allow file-read*)\n'
|
'(allow file-read*) '
|
||||||
f'{deny_rules}\n'
|
|
||||||
f'(allow file-read* (subpath "{workspace}"))\n'
|
|
||||||
f'(allow file-write* {write_expr})'
|
f'(allow file-write* {write_expr})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -189,14 +189,6 @@ class ShallowVersioningManager:
|
|||||||
# ----------------------------
|
# ----------------------------
|
||||||
# public API
|
# 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:
|
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."""
|
"""Track a file before it is edited; backup its current content if not already tracked."""
|
||||||
tracking_path = self._normalize_path(file_path)
|
tracking_path = self._normalize_path(file_path)
|
||||||
@ -214,13 +206,12 @@ class ShallowVersioningManager:
|
|||||||
backup = self._create_backup(resolved, 1)
|
backup = self._create_backup(resolved, 1)
|
||||||
self._tracked_files.add(tracking_path)
|
self._tracked_files.add(tracking_path)
|
||||||
|
|
||||||
# 归属到当前 message_id 对应的最新 snapshot;不存在则新建。
|
# Add to the most recent snapshot if one exists, otherwise create a pre-edit snapshot.
|
||||||
# 避免把本次输入的修改追加到上一个输入的 snapshot。
|
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
target_snapshot = self._find_latest_snapshot_by_message_id(message_id)
|
if self._snapshots:
|
||||||
if target_snapshot is not None:
|
latest = self._snapshots[-1]
|
||||||
target_snapshot.tracked_file_backups[tracking_path] = backup
|
latest.tracked_file_backups[tracking_path] = backup
|
||||||
self._persist_snapshot(target_snapshot)
|
self._persist_snapshot(latest)
|
||||||
else:
|
else:
|
||||||
snapshot = ShallowSnapshot(
|
snapshot = ShallowSnapshot(
|
||||||
message_id=message_id,
|
message_id=message_id,
|
||||||
@ -265,17 +256,8 @@ class ShallowVersioningManager:
|
|||||||
tracked_file_backups=tracked_file_backups,
|
tracked_file_backups=tracked_file_backups,
|
||||||
timestamp=now,
|
timestamp=now,
|
||||||
)
|
)
|
||||||
# 如果当前 message_id 已存在 snapshot(由 track_edit 预创建),则替换它,
|
self._snapshots.append(snapshot)
|
||||||
# 避免同一个输入节点留下多个 snapshot 导致 diff 只计算最后两次之间的变化。
|
self._snapshot_sequence += 1
|
||||||
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)
|
self._persist_snapshot(snapshot)
|
||||||
|
|
||||||
# Evict old snapshots while keeping the most recent MAX_SNAPSHOTS.
|
# Evict old snapshots while keeping the most recent MAX_SNAPSHOTS.
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import threading
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Sequence
|
from typing import Dict, List, Optional, Sequence
|
||||||
|
|
||||||
from config import AGENT_SKILLS_DIR, CUSTOM_SKILLS_DIR, IS_HOST_MODE, WORKSPACE_SKILLS_DIRNAME
|
from config import AGENT_SKILLS_DIR, WORKSPACE_SKILLS_DIRNAME
|
||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
|
|
||||||
logger = setup_logger(__name__)
|
logger = setup_logger(__name__)
|
||||||
@ -48,15 +48,10 @@ def ensure_workspace_skills_dir(project_path: str | Path) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def infer_private_skills_dir(data_dir: str | Path | None) -> Optional[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:
|
if not data_dir:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
if IS_HOST_MODE:
|
|
||||||
return Path(CUSTOM_SKILLS_DIR).expanduser().resolve()
|
|
||||||
data_path = Path(data_dir).expanduser().resolve()
|
data_path = Path(data_dir).expanduser().resolve()
|
||||||
if data_path.name == "data" and data_path.parent.parent.name == "workspaces":
|
if data_path.name == "data" and data_path.parent.parent.name == "workspaces":
|
||||||
return (data_path.parent.parent.parent / "agentskills").resolve()
|
return (data_path.parent.parent.parent / "agentskills").resolve()
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "astrion-frontend",
|
"name": "ai-agent-frontend",
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
你是 Astrion,一个运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。
|
|
||||||
|
|
||||||
{model_description}
|
{model_description}
|
||||||
|
|
||||||
你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
|
你是一名运行在云端服务器/用户电脑(根据工作区信息判断)根据上的全能型智能助手,专为**普通用户和开发者**设计。你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
|
||||||
|
|
||||||
- **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤
|
- **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤
|
||||||
- **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议
|
- **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
你是 Astrion,一个运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。
|
|
||||||
|
|
||||||
{model_description}
|
{model_description}
|
||||||
|
|
||||||
你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
|
你是一名运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
|
||||||
|
|
||||||
- **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤
|
- **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤
|
||||||
- **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议
|
- **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议
|
||||||
@ -207,7 +207,7 @@ def _write_custom_models(target_path: Path, entry: dict) -> Path | None:
|
|||||||
|
|
||||||
def run_wizard(force: bool = False) -> int:
|
def run_wizard(force: bool = False) -> int:
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(" Astrion 首次启动初始化向导")
|
print(" AI Agent 首次启动初始化向导")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("逐项设置启动前必须确定的内容,每项可回车使用 [默认值]。")
|
print("逐项设置启动前必须确定的内容,每项可回车使用 [默认值]。")
|
||||||
print()
|
print()
|
||||||
@ -311,7 +311,7 @@ def run_wizard(force: bool = False) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="Astrion 首次启动初始化向导")
|
parser = argparse.ArgumentParser(description="AI Agent 首次启动初始化向导")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--force",
|
"--force",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|||||||
@ -35,12 +35,7 @@ from modules.skills_manager import (
|
|||||||
sync_workspace_skills,
|
sync_workspace_skills,
|
||||||
)
|
)
|
||||||
from modules.upload_security import UploadSecurityError
|
from modules.upload_security import UploadSecurityError
|
||||||
from modules.host_sandbox_policy import (
|
from modules.host_sandbox_policy import load_policy, save_policy
|
||||||
load_policy,
|
|
||||||
save_policy,
|
|
||||||
get_macos_deny_read_paths,
|
|
||||||
get_macos_deny_read_regexes,
|
|
||||||
)
|
|
||||||
from modules.user_manager import UserWorkspace
|
from modules.user_manager import UserWorkspace
|
||||||
from core.web_terminal import WebTerminal
|
from core.web_terminal import WebTerminal
|
||||||
from config.model_profiles import get_model_context_window
|
from config.model_profiles import get_model_context_window
|
||||||
@ -54,30 +49,8 @@ from server.state import tool_approval_manager, user_question_manager
|
|||||||
from server.extensions import socketio
|
from server.extensions import socketio
|
||||||
from server.monitor import get_cached_monitor_snapshot
|
from server.monitor import get_cached_monitor_snapshot
|
||||||
from server.files import sanitize_filename_preserve_unicode
|
from server.files import sanitize_filename_preserve_unicode
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
UPLOAD_FOLDER_NAME = ".agents/user_upload"
|
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'])
|
@chat_bp.route('/api/permission-mode', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@with_terminal
|
@with_terminal
|
||||||
@ -364,8 +337,6 @@ def get_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, user
|
|||||||
"enabled": can_manage,
|
"enabled": can_manage,
|
||||||
"writable_paths": data.get("macos_writable_paths", []),
|
"writable_paths": data.get("macos_writable_paths", []),
|
||||||
"readable_extra_paths": data.get("macos_readable_extra_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'])
|
@chat_bp.route('/api/path-authorization', methods=['POST'])
|
||||||
@ -386,10 +357,11 @@ def update_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, u
|
|||||||
readable_extra = [str(x).strip() for x in readable_items if str(x).strip()]
|
readable_extra = [str(x).strip() for x in readable_items if str(x).strip()]
|
||||||
if "/" in writable or "/" in readable_extra:
|
if "/" in writable or "/" in readable_extra:
|
||||||
return jsonify({"success": False, "error": "禁止授权根目录 /"}), 400
|
return jsonify({"success": False, "error": "禁止授权根目录 /"}), 400
|
||||||
|
blocked_prefix = ("~/.ssh", "~/.gnupg", "~/.aws")
|
||||||
for p in writable + readable_extra:
|
for p in writable + readable_extra:
|
||||||
conflict = _path_conflicts_with_deny_list(p)
|
lowered = p.lower()
|
||||||
if conflict:
|
if any(lowered.startswith(prefix) for prefix in blocked_prefix):
|
||||||
return jsonify({"success": False, "error": conflict}), 400
|
return jsonify({"success": False, "error": f"禁止授权敏感目录: {p}"}), 400
|
||||||
payload = save_policy({
|
payload = save_policy({
|
||||||
"macos_writable_paths": writable,
|
"macos_writable_paths": writable,
|
||||||
"macos_readable_extra_paths": readable_extra
|
"macos_readable_extra_paths": readable_extra
|
||||||
@ -398,6 +370,4 @@ def update_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, u
|
|||||||
"success": True,
|
"success": True,
|
||||||
"writable_paths": payload.get("macos_writable_paths", []),
|
"writable_paths": payload.get("macos_writable_paths", []),
|
||||||
"readable_extra_paths": payload.get("macos_readable_extra_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", []),
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -431,14 +431,6 @@ def _record_hidden_versioning_checkpoint_after_run(
|
|||||||
debug_log(f"[Versioning] 记录快照失败: {exc}")
|
debug_log(f"[Versioning] 记录快照失败: {exc}")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
debug_log(f"[Versioning] 记录快照异常: {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(
|
def _persist_and_echo_preceding_notice(
|
||||||
@ -1018,13 +1010,6 @@ async def handle_task_with_sender(
|
|||||||
videos=videos,
|
videos=videos,
|
||||||
metadata=user_message_metadata
|
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:
|
try:
|
||||||
user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1
|
user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@ -1074,16 +1074,14 @@ def get_conversation_versioning_checkpoint_detail(conversation_id, seq: int, ter
|
|||||||
shallow_message_id = snapshot_data.get("shallow_message_id")
|
shallow_message_id = snapshot_data.get("shallow_message_id")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
debug_log(f"[Versioning][Detail] resolved shallow_message_id={shallow_message_id}")
|
files = row.get("files") or []
|
||||||
if shallow_message_id:
|
debug_log(f"[Versioning][Detail] resolved shallow_message_id={shallow_message_id} files={files}")
|
||||||
|
if shallow_message_id and isinstance(files, list):
|
||||||
shallow_manager = ShallowVersioningManager(
|
shallow_manager = ShallowVersioningManager(
|
||||||
project_path=workspace.project_path,
|
project_path=workspace.project_path,
|
||||||
data_dir=workspace.data_dir,
|
data_dir=workspace.data_dir,
|
||||||
conversation_id=normalized_id,
|
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 {}
|
stats = shallow_manager.get_diff_stats(str(shallow_message_id)) or {}
|
||||||
normalized_files: List[Dict[str, Any]] = []
|
normalized_files: List[Dict[str, Any]] = []
|
||||||
for file_item in files:
|
for file_item in files:
|
||||||
@ -1093,7 +1091,7 @@ def get_conversation_versioning_checkpoint_detail(conversation_id, seq: int, ter
|
|||||||
path = str(item.get("path") or "")
|
path = str(item.get("path") or "")
|
||||||
if path:
|
if path:
|
||||||
patch = shallow_manager.get_file_patch_lines(path, str(shallow_message_id))
|
patch = shallow_manager.get_file_patch_lines(path, str(shallow_message_id))
|
||||||
debug_log(f"[Versioning][Detail] path={path} patch_lines={len(patch.get('lines') or [])}")
|
debug_log(f"[Versioning][Detail] path={path} patch={patch}")
|
||||||
item["patch_lines"] = patch.get("lines") or []
|
item["patch_lines"] = patch.get("lines") or []
|
||||||
item["patch_truncated"] = bool(patch.get("truncated"))
|
item["patch_truncated"] = bool(patch.get("truncated"))
|
||||||
normalized_files.append(item)
|
normalized_files.append(item)
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
<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>
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Astrion</span>
|
<span>AI Assistant</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 等待动画 -->
|
<!-- 等待动画 -->
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
<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>
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Astrion</span>
|
<span>AI Assistant</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 等待动画 -->
|
<!-- 等待动画 -->
|
||||||
|
|||||||
@ -68,7 +68,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="logo">Astrion <span>v5.3</span></div>
|
<div class="logo">AI Agent <span>v5.3</span></div>
|
||||||
<div class="nav-item active">📁 项目文件</div>
|
<div class="nav-item active">📁 项目文件</div>
|
||||||
<div class="nav-item">⚙️ 管理面板</div>
|
<div class="nav-item">⚙️ 管理面板</div>
|
||||||
<div style="margin-top:20px; font-size:12px; color:#9CA3AF; font-weight:600;">TEMP</div>
|
<div style="margin-top:20px; font-size:12px; color:#9CA3AF; font-weight:600;">TEMP</div>
|
||||||
|
|||||||
@ -133,7 +133,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="logo">Astrion ✨</div>
|
<div class="logo">AI Agent ✨</div>
|
||||||
<div class="nav-item active">Project Files</div>
|
<div class="nav-item active">Project Files</div>
|
||||||
<div class="nav-item">Dashboard</div>
|
<div class="nav-item">Dashboard</div>
|
||||||
<div style="flex:1"></div>
|
<div style="flex:1"></div>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Astrion</title>
|
<title>AI Agent System</title>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
<link rel="stylesheet" href="/static/easter-eggs/flood.css">
|
<link rel="stylesheet" href="/static/easter-eggs/flood.css">
|
||||||
<link rel="stylesheet" href="/static/easter-eggs/snake.css">
|
<link rel="stylesheet" href="/static/easter-eggs/snake.css">
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>登录 - Astrion</title>
|
<title>登录 - AI Agent</title>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -255,7 +255,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="sheet-body">
|
<div class="sheet-body">
|
||||||
<div class="section-card status-card">
|
<div class="section-card status-card">
|
||||||
<strong>Astrion v2.4</strong>
|
<strong>AI Agent v2.4</strong>
|
||||||
<p>已连接 · 思考模式</p>
|
<p>已连接 · 思考模式</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
|
|||||||
@ -335,7 +335,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="sheet-body">
|
<div class="sheet-body">
|
||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
<strong>Astrion v2.4</strong>
|
<strong>AI Agent v2.4</strong>
|
||||||
<p>思考模式 · 已连接</p>
|
<p>思考模式 · 已连接</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>注册 - Astrion</title>
|
<title>注册 - AI Agent</title>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="auth-page">
|
<main class="auth-page">
|
||||||
<section class="auth-card">
|
<section class="auth-card">
|
||||||
<h1 class="auth-title">Astrion 登录</h1>
|
<h1 class="auth-title">AI Agent 登录</h1>
|
||||||
<p class="auth-subtitle">使用账号继续访问工作台</p>
|
<p class="auth-subtitle">使用账号继续访问工作台</p>
|
||||||
|
|
||||||
<div class="auth-form-group">
|
<div class="auth-form-group">
|
||||||
|
|||||||
@ -98,7 +98,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="msg.role === 'assistant'" class="assistant-message">
|
<div v-else-if="msg.role === 'assistant'" class="assistant-message">
|
||||||
<!-- 在 assistant 消息前显示 Astrion 头部 -->
|
<!-- 在 assistant 消息前显示 AI Assistant 头部 -->
|
||||||
<!-- 只有当前一条消息是 user 且当前消息有内容时才显示 -->
|
<!-- 只有当前一条消息是 user 且当前消息有内容时才显示 -->
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
@ -704,9 +704,9 @@ const stackedBlocksEnabled = computed(() => {
|
|||||||
});
|
});
|
||||||
const aiAssistantName = computed(() => {
|
const aiAssistantName = computed(() => {
|
||||||
if (!personalization.form.use_custom_names) {
|
if (!personalization.form.use_custom_names) {
|
||||||
return 'Astrion';
|
return 'AI Assistant';
|
||||||
}
|
}
|
||||||
return personalization.form.self_identify || 'Astrion';
|
return personalization.form.self_identify || 'AI Assistant';
|
||||||
});
|
});
|
||||||
const userName = computed(() => {
|
const userName = computed(() => {
|
||||||
if (!personalization.form.use_custom_names) {
|
if (!personalization.form.use_custom_names) {
|
||||||
|
|||||||
@ -24,22 +24,11 @@
|
|||||||
仅可读
|
仅可读
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">
|
<textarea
|
||||||
{{
|
|
||||||
mode === 'writable'
|
|
||||||
? '可读可写路径在 workspace-write 沙箱中可写入,只读沙箱中仅可读。'
|
|
||||||
: '仅可读路径在只读沙箱中会被加入允许读取列表;工作区可写沙箱默认已可读。'
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
<textarea
|
|
||||||
class="path-input"
|
class="path-input"
|
||||||
:value="value"
|
:value="value"
|
||||||
@input="$emit('update:value', ($event.target as HTMLTextAreaElement).value)"
|
@input="$emit('update:value', ($event.target as HTMLTextAreaElement).value)"
|
||||||
:placeholder="
|
placeholder="每行一个路径,例如:~/Desktop/agents-export"
|
||||||
mode === 'writable'
|
|
||||||
? '每行一个路径,例如:~/Desktop/agents-export'
|
|
||||||
: '每行一个路径,例如:~/Documents/reference'
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="btn" @click="$emit('save')" :disabled="saving">保存</button>
|
<button type="button" class="btn" @click="$emit('save')" :disabled="saving">保存</button>
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></span>
|
></span>
|
||||||
<div class="brand-text">
|
<div class="brand-text">
|
||||||
<span class="brand-name">Astrion</span>
|
<span class="brand-name">AI Agent</span>
|
||||||
<span class="agent-version" v-if="agentVersion">{{ agentVersion }}</span>
|
<span class="agent-version" v-if="agentVersion">{{ agentVersion }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const TUTORIAL_VERSION = 'v1';
|
|||||||
const DEFAULT_STEPS: TutorialStep[] = [
|
const DEFAULT_STEPS: TutorialStep[] = [
|
||||||
{
|
{
|
||||||
id: 'welcome',
|
id: 'welcome',
|
||||||
title: '欢迎使用 Astrion',
|
title: '欢迎使用 AI Agent 系统',
|
||||||
description: '我们将用 2-3 分钟带你快速了解核心功能。',
|
description: '我们将用 2-3 分钟带你快速了解核心功能。',
|
||||||
target: null,
|
target: null,
|
||||||
mode: 'info',
|
mode: 'info',
|
||||||
|
|||||||
@ -402,7 +402,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-right">
|
<div class="status-right">
|
||||||
<span style="opacity: 0.6;">Astrion Terminal Monitor v1.0</span>
|
<span style="opacity: 0.6;">AI Agent Terminal Monitor v1.0</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -555,7 +555,7 @@
|
|||||||
term.writeln('\x1b[1;36m║ 🤖 AI Terminal Monitor v1.0 ║\x1b[0m');
|
term.writeln('\x1b[1;36m║ 🤖 AI Terminal Monitor v1.0 ║\x1b[0m');
|
||||||
term.writeln('\x1b[1;36m╚════════════════════════════════════════╝\x1b[0m');
|
term.writeln('\x1b[1;36m╚════════════════════════════════════════╝\x1b[0m');
|
||||||
term.writeln('');
|
term.writeln('');
|
||||||
term.writeln('\x1b[33m正在连接到 Astrion...\x1b[0m');
|
term.writeln('\x1b[33m正在连接到AI Agent...\x1b[0m');
|
||||||
term.writeln('');
|
term.writeln('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user