Merge remote-tracking branch 'origin/feat/originui' into feat/originui

This commit is contained in:
肖应宇 2026-03-20 09:22:19 +08:00
commit 6335424aca
4 changed files with 58 additions and 109 deletions

View File

@ -263,11 +263,11 @@ docker-logs-gateway:
# ==========================================
# Docker Publish Command
# ==========================================
# Usage: make docker-publish VER=v220.20251202 SVC=frontend
# Example: make docker-publish VER=v220.20251202 SVC=frontend
# Usage: make docker-publish VER=[version] SVC=[service name] [PUSH=1]
# Example: make docker-publish VER=v2.0.20251202 SVC=frontend PUSH=0
docker-publish:
@if [ -z "$(VER)" ]; then \
echo "✗ VER is required (e.g. v220.20251202)"; \
echo "✗ VER is required (e.g. v2.0.20251202)"; \
exit 1; \
fi
@if [ -z "$(SVC)" ]; then \
@ -293,9 +293,13 @@ docker-publish:
echo "✗ Docker build failed"; \
exit 1; \
fi; \
docker push $$IMAGE; \
if [ $$? -ne 0 ]; then \
echo "✗ Docker push failed"; \
exit 1; \
fi; \
echo "✓ Docker image $$IMAGE built and pushed successfully"
if [ "$(PUSH)" = "0" ]; then \
echo "✓ Docker image $$IMAGE built successfully (not pushed)"; \
else \
docker push $$IMAGE; \
if [ $$? -ne 0 ]; then \
echo "✗ Docker push failed"; \
exit 1; \
fi; \
echo "✓ Docker image $$IMAGE built and pushed successfully"; \
fi

View File

@ -27,6 +27,7 @@ import { DevTodoList } from "@/components/workspace/dev-todo-list";
import { IframeTestPanel } from "@/components/workspace/iframe-test-panel";
import { InputBox } from "@/components/workspace/input-box";
import { MessageList } from "@/components/workspace/messages";
import { MessageListSkeleton } from "@/components/workspace/messages/skeleton";
import { ThreadContext } from "@/components/workspace/messages/context";
import { ThreadTitle } from "@/components/workspace/thread-title";
import { TodoList } from "@/components/workspace/todo-list";
@ -164,6 +165,10 @@ export default function ChatPage() {
const [hasSubmitted, setHasSubmitted] = useState(false);
const suppressExistingThreadPrefetchUi = reuseExistingThread && !hasSubmitted;
const suppressNewThreadSubmitUi =
isNewThread && createNewSession && hasSubmitted;
const suppressConversationUi =
suppressExistingThreadPrefetchUi || suppressNewThreadSubmitUi;
useEffect(() => {
const pageTitle = isNewThread
@ -171,7 +176,7 @@ export default function ChatPage() {
: thread.values?.title && thread.values.title !== "Untitled"
? thread.values.title
: t.pages.untitled;
if (thread.isThreadLoading && !suppressExistingThreadPrefetchUi) {
if (thread.isThreadLoading && !suppressConversationUi) {
document.title = `Loading... - ${t.pages.appName}`;
} else {
document.title = `${pageTitle} - ${t.pages.appName}`;
@ -183,19 +188,21 @@ export default function ChatPage() {
t.pages.appName,
thread.values.title,
thread.isThreadLoading,
suppressExistingThreadPrefetchUi,
suppressConversationUi,
]);
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
useEffect(() => {
setArtifacts(thread.values.artifacts);
if (
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
autoSelectFirstArtifact
) {
if (thread?.values?.artifacts?.length > 0) {
setAutoSelectFirstArtifact(false);
selectArtifact(thread.values.artifacts[0]!);
if (!suppressConversationUi) {
setArtifacts(thread.values.artifacts);
if (
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
autoSelectFirstArtifact
) {
if (thread?.values?.artifacts?.length > 0) {
setAutoSelectFirstArtifact(false);
selectArtifact(thread.values.artifacts[0]!);
}
}
}
}, [

View File

@ -18,6 +18,29 @@ import type {
AgentThreadState,
} from "./types";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function waitForThreadStateToBeReadable(
apiClient: ReturnType<typeof getAPIClient>,
threadId: string,
timeoutMs = 3000,
) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const state = await apiClient.threads.getState<AgentThreadState>(threadId);
if ((state.values.messages?.length ?? 0) > 0) {
return;
}
} catch {
// Ignore transient 404 / not-ready errors while the new thread is being persisted.
}
await sleep(100);
}
}
export function useThreadStream({
threadId,
isNewThread,
@ -188,6 +211,11 @@ export function useSubmitThread({
},
},
);
if (createNewSession && isNewThread && threadId) {
await waitForThreadStateToBeReadable(apiClient, threadId);
}
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
afterSubmit?.();
},

90
memo.md
View File

@ -1,90 +0,0 @@
# 当前改动总结2026-03-09
## TODO备忘AIO sandbox端口分配并发竞态
- 问题sandbox容器启动时端口分配get_free_port + docker run非原子操作存在并发竞态。
- 多个会话并发检测到端口空闲,可能同时尝试分配同一端口,导致后一个容器启动失败(端口已被占用)。
- 建议:
- 增加端口分配锁如文件锁、Redis等保证端口分配与容器启动原子性。
- 或在容器启动失败后自动重试分配新端口。
- 适用于高并发场景,低并发下概率极低。
# 当前改动总结2026-03-09
## 7) 近期补充变更2026-03-13
- langgraph 会话持久化:
- 支持 langgraph API 层会话落盘,重启后历史线程/消息可恢复。
- 通过 .langgraph_api 挂载和主进程 exec 启动,保证 SIGTERM 优雅关闭和持久化。
- skill 扫描范围扩展:
- skill 扫描目录新增 /mnt/user-data/uploads 路径,支持用户上传 skill-yaml、skill 文件自动纳入扫描。
- 兼容 skill-package.yaml、skill.zip 等多种格式,自动生成 skill 目录。
- 外部创建 langgraph 会话:
- 支持通过 API/外部服务创建 langgraph 会话thread_id 可由外部指定。
- 前端/第三方系统可直接初始化会话并绑定 skill_id实现多入口集成。
# 当前改动总结2026-03-09
## 6) Skill YAML 自动导入与远程初始化2026-03-13
- 目标:支持上传 skill-yaml.yaml一键导入为 skill 目录,并支持 skill_id/languageType 参数自动远程初始化。
- 前端:
- 新增 materializeSkillYaml/ bootstrapRemoteSkill API支持 skill-yaml 文件解析与远程内容拉取。
- 在 chats/[thread_id]/page.tsx 页面加载时自动触发 skill 初始化(有 skill_id 参数时),并用 useEffect/Ref 保证只初始化一次。
- 提交时增加空消息 guard避免页面初始化时误触发 submit。
- 上传文件卡片支持 YAML 文件解析materializeSkillYaml但按钮默认注释后续可按需开放。
- 初始化期间禁用输入框UI 显示“正在初始化 Skill 文件...”或失败提示。
- 后端:
- gateway/config.py 增加 skill_content_api_url 配置,支持环境变量覆盖。
- routers/skills.py 新增 /api/skills/materialize-yaml 与 /api/skills/bootstrap-remote 两个 POST 接口,分别支持本地 YAML 解析与远程内容拉取+目录生成。
- skill_yaml_importer.py 增强解析器,支持 package.structure、path/name/children、root sentinel、别名等多种 YAML schema兼容复杂 skill-package.yaml。
- 解析异常时返回详细错误,前端可捕获并提示。
- 流程说明:
1. 用户上传 skill-yaml.yaml可在文件卡片触发“导入为 Skill 目录”APImaterializeSkillYaml前端已注释掉相关代码
2. 页面有 skill_id/languageType 参数时,自动调用 bootstrapRemoteSkill拉取远程 YAML 并生成目录。
3. skill 初始化总在 skill 扫描前完成,且只触发一次,支持新/旧线程。
4. 提交时空消息 guard避免页面加载时误触发 submit。
- 文件:
- frontend/src/app/workspace/chats/[thread_id]/page.tsx
- frontend/src/core/skills/api.ts
- frontend/src/components/workspace/messages/message-list-item.tsx
- frontend/src/core/threads/hooks.ts
- backend/src/gateway/config.py
- backend/src/gateway/routers/skills.py
- backend/src/gateway/skill_yaml_importer.py
- 效果skill-yaml 自动导入、远程 skill 初始化、页面加载触发、解析器兼容多 schema前后端 API 完整闭环。
# 当前改动总结2026-03-09
## 1) AIO sandbox 网络访问修复(已完成)
- 问题:容器内访问 `localhost` 实际指向容器自身,无法访问宿主机上的 sandbox 端口。
- 调整:将 sandbox 访问地址改为 `host.docker.internal`
- 文件:`backend/src/community/aio_sandbox/local_backend.py`
- 影响:`create()` / `discover()` 走宿主机映射端口可达。
## 2) AIO sandbox Docker 权限与连通性修复(已完成)
- 问题:`gateway` / `langgraph` 容器无法直接访问 Docker daemon无法管理 AIO sandbox 容器。
- 调整:为两个服务挂载 Docker socket并设置 `DOCKER_HOST`
- 文件:`docker/docker-compose-dev.yaml`
- 关键配置:
- volume: `/var/run/docker.sock:/var/run/docker.sock:ro`
- env: `DOCKER_HOST=unix:///var/run/docker.sock`
## 3) 会话持久化最终生效方案(已验证)
- 目标:`make docker-stop && make docker-start` 后 `/workspace/chats` 保留历史线程。
- 最终生效改动(都在 `langgraph` 服务):
1. 启动命令改为 `exec uv run langgraph dev ... --no-reload ...`
2. 挂载持久化目录:`../backend/.langgraph_api:/app/backend/.langgraph_api`
- 文件:`docker/docker-compose-dev.yaml`
### 原理说明
- `.langgraph_api` 挂载:把 inmem runtime 的落盘文件映射到宿主机,容器重建后仍保留。
- `exec`:让 LangGraph 主进程直接接收 `SIGTERM`,触发优雅关闭与 `PersistentDict` 写盘。
- `--no-reload`:避免热重载多进程导致的停机时序问题,保证落盘稳定。
## 5) Docker下目录权限修复已完成
- 目标:确保 sandbox 容器/Pod 挂载的 `/mnt/user-data` 及其子目录可被非 root 用户正常读写,避免上传/写入失败。
- 文件:
- `backend/src/community/aio_sandbox/aio_sandbox_provider.py`
- `backend/src/community/aio_sandbox/local_backend.py`
- `docker/provisioner/app.py`
- 关键措施:
1. 启动 sandbox 时自动创建 thread 挂载目录并 `chmod 777`,解析为 host 路径,保证权限生效。
2. 容器启动后用 `docker exec` 执行 `mkdir -p``chmod 777`,多次重试,确保 `/mnt/user-data/uploads/workspace/outputs` 可写。
3. K8s Pod spec 新增 init container以 root 权限初始化 `/mnt/user-data``chmod -R 777`,安全上下文限制特权。
- 效果sandbox 挂载目录无论本地还是 K8s均可被非 root 用户正常写入,上传/输出/工作区权限问题彻底解决。