diff --git a/SKILL.md b/SKILL.md index 3421d8b..52a2926 100644 --- a/SKILL.md +++ b/SKILL.md @@ -169,3 +169,9 @@ description: 组织项目的多个 AI 会话并行工作,将进度、证据和 主控检查后记录结论、交接及证据才完成;完成后由获授权总控调用宿主真实归档工具,再保存回执,服务记录不冒充归档操作。隔离测试不能代替真实会话绑定与往返,尚未验证必须明示;没有真实待决定事项时不得为验收创建生产决策或会话。 已有专门人类入口时,主控、副主控和执行者不得直接向用户提问;问题、待选择、待授权及关键结论定向交唯一入口,由入口理解并与人类交互,再整理给主控,不经过底层事件队列。需选项先建真实决策卡,入口简短链接。决策页内嵌咨询只回答该决策范围问题,不另设一般项目入口;人类决定和审批仍走决策卡与唯一入口。 + +## 网页草稿持久边界 + +沟通输入、决策意见与选择、决策咨询输入应在浏览器本地持久保存,按实例、目标会话、决策与内容版本隔离;退出页面、刷新及重新登录后恢复同范围草稿。草稿不是已提交事实,不上传草稿,不将token写入草稿记录。仅收到成功确认后清除对应提交的草稿,或由用户明确清除单项;失败和结果未知保留正文与幂等重试信息,禁止自动重发。内容版本变化保留旧稿供明确重确认,不能静默覆盖新版本或沿用旧选择作为批准。 + +本地存储不可用时如实提示,不能宣称已保存;本地浏览器数据被清理、设备更换或旧版未持久化的已丢稿不承诺恢复。提供明确的单项草稿清除入口。已有唯一人类入口时,相关限制与所需用户操作仍由该入口沟通。 diff --git a/agent_control/server.py b/agent_control/server.py index 4eafa48..bd17e9c 100644 --- a/agent_control/server.py +++ b/agent_control/server.py @@ -231,6 +231,11 @@ class Handler(BaseHTTPRequestHandler): state['primary_session_id'] = self.server.primary_session_id state['metrics'] = self.server.metrics.snapshot() state['eta_clock'] = dict(self.server.eta_clock.status) + try: + entry = re.search(r']*\bsrc="(/assets/[^"/]+\.js)"', (self.server.web_root/'index.html').read_text(encoding='utf-8')) + state['frontend_entrypoint'] = entry.group(1) if entry else None + except OSError: + state['frontend_entrypoint'] = None self.output(200, state) elif parts.path == '/api/decision-bridge-bindings' and not parts.query: self.output(200,self.server.store.decision_bindings()) diff --git a/assets/dashboard/src/App.tsx b/assets/dashboard/src/App.tsx index 0097cde..8e7ba3e 100644 --- a/assets/dashboard/src/App.tsx +++ b/assets/dashboard/src/App.tsx @@ -13,6 +13,7 @@ import { Workbench } from '@/lib/context' import { request, errorText } from '@/lib/api' import type { State, AttentionItem } from '@/lib/types' import { projectTitle, date } from '@/lib/types' +import VersionNotice from '@/components/version-notice' import Overview from '@/pages/overview' import Tasks from '@/pages/tasks' import Cluster from '@/pages/cluster' @@ -44,7 +45,7 @@ function Shell({ token, initial, logout }: { token: string; initial: State; logo
{isChat ?

项目沟通

询问进度、讨论方案,或告诉我你的新想法。

: {navigation.find(n => n.url === location.pathname)?.title || '总览'}}
{isChat ? :
{error ? '连接中断' : '实时同步'}{date(state.updated)}
}
-
{error && {error} 当前显示上次同步的数据。}} />} />} />} />} />} />} />
+
{error && {error} 当前显示上次同步的数据。}} />} />} />} />} />} />} />
{!isChat && } } diff --git a/assets/dashboard/src/components/decision-chat.tsx b/assets/dashboard/src/components/decision-chat.tsx index 73d2fe0..06bfb3d 100644 --- a/assets/dashboard/src/components/decision-chat.tsx +++ b/assets/dashboard/src/components/decision-chat.tsx @@ -3,15 +3,21 @@ import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { request, errorText, messageKey } from '@/lib/api' import type { Decision, Message } from '@/lib/types' +import { useLocalDraft } from '@/lib/use-local-draft' +import { useWorkbench } from '@/lib/context' import type { ChatDraft } from '@/pages/decision-ui' -export default function DecisionChat({ decision, token, drafts }: { decision: Decision; token: string; drafts: Map }) { +export default function DecisionChat({ decision, token }: { decision: Decision; token: string }) { const id = decision.id, bound = Boolean(decision.session_id && decision.session_thread_id) const readOnly = decision.status === 'completed' || decision.archive_status === 'archived' const [messages, setMessages] = useState([]), [error, setError] = useState(''), [loaded, setLoaded] = useState(false), [, redraw] = useState(0) const refresh = useRef<() => Promise>(async () => {}) const scroll = useRef(null) - if (!drafts.has(id)) drafts.set(id, { text: '', sending: false, error: '' }) - const draft = drafts.get(id)! + const { state } = useWorkbench() + const revision = decision.revision ?? 1 + const local = useLocalDraft(state.instance_id && Number.isInteger(decision.revision) && (decision.revision ?? 0) > 0 ? [state.instance_id, 'consultation', id, decision.session_id ?? 'unbound', String(revision)] : null, {text:'',sending:false,error:'',revision}, (v): v is ChatDraft & {revision:number} => Boolean(v && typeof v === 'object' && typeof (v as ChatDraft).text === 'string'), true) + const draft = local.value + const sending = useRef(false) + function persist() {local.save(draft);redraw(n => n+1)} useEffect(() => { if (!bound) return const controller = new AbortController(); let busy = false @@ -26,18 +32,21 @@ export default function DecisionChat({ decision, token, drafts }: { decision: De return () => { clearInterval(timer); controller.abort() } }, [id, token, bound]) async function send() { - if (!bound || readOnly || draft.sending || !draft.text.trim()) return + if (!bound || readOnly || !local.ready || draft.revision !== revision || sending.current || draft.sending || !draft.text.trim()) return const text = draft.text if (!draft.attempt || draft.attempt.text !== text) draft.attempt = { text, key: messageKey() } + if (!local.save(draft)) return + sending.current=true draft.sending = true; draft.error = ''; redraw(n => n + 1) - try { await request(token, `/api/decisions/${encodeURIComponent(id)}/consultation`, { text, client_key: draft.attempt.key }); if (draft.text === text) draft.text = ''; draft.attempt = undefined; await refresh.current() } + try { await request(token, `/api/decisions/${encodeURIComponent(id)}/consultation`, { text, client_key: draft.attempt.key }); local.clear(); await refresh.current() } catch (cause) { draft.error = `${errorText(cause)} 草稿已保留,可重试同一消息。` } - finally { draft.sending = false; redraw(n => n + 1) } + finally { sending.current=false; draft.sending = false; redraw(n => n + 1) } } return

本决策咨询

{!bound ? '咨询会话待接通,暂不能发送。' : readOnly ? '本决策已完成,咨询历史只读。' : '消息仅发送到本决策绑定的咨询会话。绑定不代表回复链路已验收。'}

{messages.map(message =>

{message.source === 'human' ? '你' : '咨询助手'}

{message.text}

{message.source === 'human' && ['queued','claimed'].includes(message.status) &&

等待回复…

}{['failed','uncertain'].includes(message.status) &&

回复尚未确认,请勿重复发送。

}
)}{!messages.length &&

{!bound ? '暂无已接通的咨询会话。' : loaded ? '暂无咨询消息。' : '正在读取咨询历史…'}

}
{(error || draft.error) &&

{draft.error || error}

} - {!readOnly &&
{ event.preventDefault(); void send() }}>