@arcfold/dsh-method-ui 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -2
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/lib/client.js +4237 -0
  5. package/package.json +35 -4
  6. package/src/Brand.mjs +17 -0
  7. package/src/CollaborationView.mjs +56 -0
  8. package/src/DesktopModels.mjs +40 -0
  9. package/src/DesktopSetup.mjs +48 -0
  10. package/src/GoalDock.mjs +130 -0
  11. package/src/GoalWorkList.mjs +52 -0
  12. package/src/ImportDock.mjs +172 -0
  13. package/src/MethodDock.mjs +145 -0
  14. package/src/ModelSettings.mjs +144 -0
  15. package/src/OpportunityDock.mjs +66 -0
  16. package/src/UnderstandingDock.mjs +117 -0
  17. package/src/browse-scope.mjs +6 -0
  18. package/src/browse-source.mjs +31 -0
  19. package/src/client.mjs +138 -0
  20. package/src/desktop-setup-host.mjs +30 -0
  21. package/src/desktop-workspace.mjs +8 -0
  22. package/src/goal-host.mjs +133 -0
  23. package/src/goal-locales.mjs +78 -0
  24. package/src/goal-source.mjs +72 -0
  25. package/src/import-host.mjs +98 -0
  26. package/src/import-locales.mjs +126 -0
  27. package/src/import-source.mjs +83 -0
  28. package/src/index.mjs +127 -0
  29. package/src/locales.mjs +153 -0
  30. package/src/main-session.mjs +7 -0
  31. package/src/method-source.mjs +82 -0
  32. package/src/model-host.mjs +56 -0
  33. package/src/model-settings-locales.mjs +91 -0
  34. package/src/model-settings-source.mjs +119 -0
  35. package/src/opportunity-host.mjs +118 -0
  36. package/src/opportunity-locales.mjs +34 -0
  37. package/src/opportunity-source.mjs +56 -0
  38. package/src/style.css +99 -0
  39. package/src/tree-client.mjs +15 -0
  40. package/src/tree-host.mjs +39 -0
  41. package/src/tree-source.mjs +65 -0
  42. package/src/tree-style.css +19 -0
  43. package/src/tree-view.mjs +278 -0
  44. package/src/understanding-host.mjs +123 -0
  45. package/src/understanding-locales.mjs +38 -0
  46. package/src/understanding-source.mjs +64 -0
@@ -0,0 +1,39 @@
1
+ import { TypertRemoteService, RemoteError } from '@deepseek-ai/dsh-typert-protocol';
2
+ // This package is installed separately from core. Its wire declarations must
3
+ // not reach back through the development monorepo layout. The gateway test
4
+ // checks this public list against the core adapter on every candidate.
5
+ export const TREE_RPC_ENDPOINTS = ['session.current', 'session.selectBranch', 'tree.list', 'node.details', 'turn.details', 'source.read', 'node.rename', 'node.move', 'node.status', 'turn.correct', 'turn.undoCreate', 'turn.undoStatus', 'item.confirm', 'item.edit', 'item.withdraw', 'item.undoCorrection', 'hook.disable', 'signal.correct', 'snapshots.list', 'snapshots.preview', 'snapshots.restore', 'metrics.read', 'reports.list', 'reports.details', 'reports.export', 'parameters.set', 'receipts.read'];
6
+ const NAMESPACE = 'arcfoldTreeV3', SERVICE = 'arcfoldTreeWeb';
7
+ const messages = {
8
+ CANDIDATE_CAPACITY: '这支目标的候选仍超过2000个,请选择更小的一支;尚未调用模型。', SESSION_BUSY: '当前对话仍在处理,请稍后刷新再选择范围。', CONTINUATION_UNAVAILABLE: '这条消息已继续或发送状态待核对,不能重复发送。',
9
+ REVISION_CONFLICT: '内容已变化,请刷新后再决定。', VERSION_CONFLICT: '这个版本已变化,请刷新。', IDEMPOTENCY_CONFLICT: '此提交编号已经用于另一份操作,未再次执行。',
10
+ NEED_ASSIGNMENT_CORRECTION: '这个目标已有后续经历,请改用纠正本轮归属。', RESTORE_CONFLICT: '预览后文件已变化,请重新预览。',
11
+ FORBIDDEN: '当前连接无权执行此操作。', BROWSE_SCOPE: '这项内容不在当前可查看范围。', SOURCE_CHANGED: '原始证据已经不可用,请刷新。',
12
+ USER_PROTECTED: '用户确认的内容受保护。', CORRECTION_CHANGED: '更正后的内容已有变化,请查看历史后再决定。', SNAPSHOT_UNAVAILABLE: '当前工作区尚不能恢复这份快照。', INVALID_FIELD: '请检查填写内容。', INVALID_SHAPE: '操作字段不完整或不受支持。',
13
+ TREE_CYCLE: '不能把目标移到自己的下级。', ROOT_INVARIANT: '不能这样改变根目标。', CONFIRM_INFERENCE_ONLY: '只有助手推断的理解需要确认。', ITEM_RECHECK_REQUIRED: '归属已变化,请核对并保存当前内容后再使用。', ENDPOINT_NOT_FOUND: '不存在这项操作。',
14
+ };
15
+ export function createTreeHandler({ service, connection, inflight = new Map() }) {
16
+ return async (endpoint, payload, signal, peer) => {
17
+ try {
18
+ if (!peer || peer !== connection.browserPeer || peer.principal?.kind !== 'owner' || peer.interactions !== 'answer' || peer.signal?.aborted !== false || signal?.aborted !== false) throw new RemoteError('TRUSTED_BROWSER_REQUIRED', '仅接受当前已认证网页的请求。', {});
19
+ if (!service || !payload || typeof payload.sessionId !== 'string' || !payload.sessionId.trim()) throw new RemoteError('TREE_UNAVAILABLE', '请打开当前个人助手的聊天。', {});
20
+ const { sessionId, ...request } = payload, context = { actor: { kind: 'user', id: service.repo.userId }, sessionId, signal: AbortSignal.any([signal, peer.signal]), peer };
21
+ const key = request.requestId && peer.id + ':' + request.requestId, hash = JSON.stringify([sessionId, endpoint, request]);
22
+ const active = key && inflight.get(key); if (active) { if (active.hash !== hash) throw new RemoteError('IDEMPOTENCY_CONFLICT', messages.IDEMPOTENCY_CONFLICT, {}); return await active.promise; }
23
+ const promise = service.dispatch(endpoint, request, context); if (key) inflight.set(key, { hash, promise });
24
+ try { return await promise; } finally { if (key && inflight.get(key)?.promise === promise) inflight.delete(key); }
25
+ } catch (error) {
26
+ if (error instanceof RemoteError) throw error;
27
+ const code = Object.hasOwn(messages, error?.code) ? error.code : 'TREE_REQUEST_FAILED';
28
+ throw new RemoteError(code, messages[code] ?? '操作尚未确认,请刷新查看记录。没有自动重发。', { status: error?.statusCode === 403 ? 403 : 409 });
29
+ }
30
+ };
31
+ }
32
+ class TreeWeb extends TypertRemoteService {
33
+ inflight = new Map();
34
+ constructor(ctx) { super(ctx, SERVICE, { namespace: NAMESPACE }); }
35
+ dispatch(endpoint, request) { const { signal, peer } = this.ctx.invocation ?? {}; return createTreeHandler({ service: this.ctx.arcfoldTreeV3, connection: this.ctx.connection, inflight: this.inflight })(endpoint, request, signal, peer); }
36
+ }
37
+ for (const endpoint of TREE_RPC_ENDPOINTS) Object.defineProperty(TreeWeb.prototype, endpoint, { value(request) { return this.dispatch(endpoint, request); } });
38
+ export const treeInvocations = TREE_RPC_ENDPOINTS.map(method => ({ id: `@arcfold/dsh-method-ui#${NAMESPACE}/${method}`, service: SERVICE, namespace: NAMESPACE, method, invocation: { kind: 'direct' }, parameters: [{ name: 'request', wire: 'request', source: 'json', codec: { mode: 'src-json' } }], result: { mode: 'src-json' } }));
39
+ export function installTreeWeb(ctx) { return ctx.inject(['arcfoldTreeV3', 'connection'], owner => { new TreeWeb(owner); }); }
@@ -0,0 +1,65 @@
1
+ /** Browser reads may refresh; writes never retry automatically. */
2
+ export function createTreeSource({ rpc, sessionId, subscribeReset = () => () => {}, intervalMs = 5000 }) {
3
+ let snapshot = { loading: true, error: null, nodes: [], treeRevision: 0, current: { paths: [], turnIds: [] }, operations: {}, selection: null, epoch: 0, refreshRevision: 0 };
4
+ let disposed = false, generation = 0, timer, reset;
5
+ const listeners = new Set(), reads = new Set(), writes = new Map(), pending = new Map();
6
+ const stopped = () => Object.assign(new Error('连接已结束,请重新打开。'), { code: 'TREE_READ_CANCELLED' });
7
+ const publish = patch => { snapshot = { ...snapshot, ...patch }; listeners.forEach(listener => listener()); };
8
+ const operation = (id, value) => publish({ operations: { ...snapshot.operations, [id]: { ...snapshot.operations[id], ...value } } });
9
+ async function call(endpoint, payload, signal) {
10
+ if (disposed) throw stopped();
11
+ const result = await rpc.call('/api', 'arcfoldTreeV3/' + endpoint, { args: { request: { ...payload, sessionId } } }, signal);
12
+ if (result?.ok !== true) throw Object.assign(new Error(result?.error?.message ?? '请求未确认,请读取操作记录。'), { code: result?.error?.code ?? 'TREE_UNKNOWN', remote: result?.ok === false });
13
+ return result.value;
14
+ }
15
+ async function read(endpoint, payload = {}) {
16
+ const controller = new AbortController(), epoch = snapshot.epoch; reads.add(controller);
17
+ try { const result = await call(endpoint, payload, controller.signal); if (disposed || controller.signal.aborted || snapshot.epoch !== epoch) throw stopped(); return result; }
18
+ finally { reads.delete(controller); }
19
+ }
20
+ function invalidate() { generation++; reads.forEach(c => c.abort()); publish({ epoch: snapshot.epoch + 1, nodes: [], selection: null, current: { paths: [], turnIds: [] }, loading: true }); }
21
+ async function refresh() {
22
+ if (disposed) return; const serial = ++generation;
23
+ try {
24
+ // Read every stable page. A mid-read revision conflict is visible and
25
+ // does not splice two revisions of the tree into a misleading view.
26
+ let nodes = [], cursor, treeRevision;
27
+ do { const page = await read('tree.list', { ...(cursor ? { cursor } : {}), limit: 100 }); nodes.push(...page.nodes); cursor = page.nextCursor; treeRevision = page.treeRevision; } while (cursor);
28
+ const current = await read('session.current');
29
+ if (!disposed && serial === generation) publish({ nodes, current, treeRevision, loading: false, error: null, refreshRevision: snapshot.refreshRevision + 1 });
30
+ } catch (error) { if (!disposed && serial === generation) publish({ loading: false, error: error.message }); }
31
+ }
32
+ async function readback(requestId) {
33
+ const receipt = await read('receipts.read', { requestId }); operation(requestId, { state: receipt.state, receipt });
34
+ if (['success', 'completed'].includes(receipt.state)) { pending.delete(requestId); void refresh(); }
35
+ return receipt;
36
+ }
37
+ function act(endpoint, payload, requestId = globalThis.crypto.randomUUID()) {
38
+ const hash = JSON.stringify([endpoint, payload]);
39
+ const old = writes.get(requestId); if (old) {
40
+ if (old.hash !== hash) return Promise.reject(Object.assign(new Error('这次提交编号已用于另一项操作。'), { code: 'IDEMPOTENCY_CONFLICT' }));
41
+ return old.promise;
42
+ }
43
+ const controller = new AbortController(); pending.set(requestId, { endpoint, payload, hash }); operation(requestId, { endpoint, state: 'pending', message: '正在提交,尚未确认' });
44
+ const promise = (async () => {
45
+ try { const receipt = await call(endpoint, { ...payload, requestId }, controller.signal); operation(requestId, { state: receipt.state, receipt, message: receipt.state === 'success' ? '已保存' : '结果尚待确认' }); if (receipt.state === 'success') pending.delete(requestId); await refresh(); return receipt; }
46
+ catch (error) {
47
+ operation(requestId, { state: error.remote ? 'conflict' : 'unknown', message: error.message, code: error.code });
48
+ // Lost replies may follow a successful server mutation. Only read its
49
+ // receipt; a missing receipt is not permission to resend the write.
50
+ if (!error.remote && !disposed) { try { const found = await readback(requestId); if (found.state === 'success') return found; } catch {} }
51
+ throw error;
52
+ }
53
+ })(); writes.set(requestId, { hash, promise, controller }); return promise;
54
+ }
55
+ function stop() { clearInterval(timer); timer = undefined; reset?.(); reset = undefined; }
56
+ return {
57
+ getSnapshot: () => snapshot,
58
+ subscribe(listener) { if (disposed) return () => {}; listeners.add(listener); if (listeners.size === 1) { void refresh(); timer = setInterval(() => void refresh(), intervalMs); reset = subscribeReset(() => { invalidate(); void refresh(); for (const id of pending.keys()) void readback(id).catch(() => {}); }); } return () => { listeners.delete(listener); if (!listeners.size) stop(); }; },
59
+ read, act, readback, refresh,
60
+ select: nodeId => publish({ selection: nodeId }),
61
+ openDetails: nodeId => publish({ selection: nodeId, detailsNodeId: nodeId }),
62
+ closeDetails: () => publish({ detailsNodeId: null }),
63
+ dispose() { disposed = true; stop(); reads.forEach(c => c.abort()); writes.forEach(w => w.controller.abort()); listeners.clear(); },
64
+ };
65
+ }
@@ -0,0 +1,19 @@
1
+ /* The native shell owns brand, theme, chat and draft state. Only the Arcfold view is laid out here. */
2
+ .arcfold-tree{font:inherit;font-size:13px;line-height:1.65;color:var(--dsw-alias-text-primary,inherit);--tree-bg:var(--dsw-alias-bg-base,Canvas);--tree-line:var(--dsw-alias-border-l2,color-mix(in srgb,currentColor 12%,transparent));--tree-faint:var(--dsw-alias-border-l1,color-mix(in srgb,currentColor 5%,transparent));--tree-soft:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,currentColor 4%,transparent));--tree-muted:var(--dsw-alias-text-tertiary,color-mix(in srgb,currentColor 55%,transparent));--tree-secondary:var(--dsw-alias-text-secondary,color-mix(in srgb,currentColor 70%,transparent));--tree-accent:#658f77;--tree-danger:#bd6353}
3
+ .arcfold-tree *{box-sizing:border-box}.arcfold-tree button,.arcfold-tree input,.arcfold-tree textarea,.arcfold-tree select{font:inherit;color:inherit;background:var(--tree-bg);border:1px solid var(--tree-line);border-radius:8px;padding:6px 12px;max-width:100%}
4
+ .arcfold-tree button{cursor:pointer;min-height:34px;line-height:1.5;font-weight:500}.arcfold-tree button:hover{background:var(--tree-soft)}.arcfold-tree button:disabled{opacity:.5;cursor:default}.arcfold-tree :focus-visible{outline:2px solid var(--tree-accent);outline-offset:3px}.arcfold-tree h2,.arcfold-tree h3,.arcfold-tree p{margin:0}.arcfold-tree h2{font-size:21px;font-weight:600;line-height:1.5}.arcfold-tree h3{font-size:12px;font-weight:600;color:var(--tree-secondary)}.arcfold-tree h4{font-size:12px;margin:12px 0 4px}.arcfold-tree small{font-size:11px;font-weight:400;color:var(--tree-muted)}.arcfold-tree pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit}.arcfold-tree summary{cursor:pointer}.tree-muted,.tree-empty{color:var(--tree-muted)}.tree-empty{padding:7px 0 4px;font-size:12px}.arcfold-tree [role=alert]{color:var(--tree-danger);padding:8px 16px}.arcfold-tree [role=status]{font-size:12px}.arcfold-tree .tree-primary{background:var(--dsw-alias-text-primary,#0f1115);color:var(--tree-bg);border-color:transparent}.arcfold-tree .tree-primary:hover{opacity:.85}.arcfold-tree .tree-danger{color:var(--tree-danger);border-color:color-mix(in srgb,var(--tree-danger) 35%,transparent);background:color-mix(in srgb,var(--tree-danger) 3%,var(--tree-bg))}.arcfold-tree .tree-danger:hover{background:color-mix(in srgb,var(--tree-danger) 10%,var(--tree-bg))}
5
+ /* Native data attributes are stable shell boundaries; hide, never unmount, the saved chat draft. */
6
+ [data-conversation-scroll]:has(.tree-view){overflow:hidden}
7
+ [data-conversation-scroll]:has(.tree-view) [data-composer-seat]{display:none}
8
+ .tree-view{container-type:inline-size;display:flex;flex-direction:column;flex:1;min-height:0;height:var(--dsh-conversation-viewport-height,calc(100dvh - 76px));max-height:var(--dsh-conversation-viewport-height,calc(100dvh - 76px));width:100%;background:var(--tree-bg)}
9
+ .tree-toolbar{height:52px;flex:none;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 24px;border-bottom:1px solid var(--tree-faint)}.tree-toolbar nav{display:flex;gap:7px;min-width:0}.tree-toolbar button{border:0;min-height:30px;padding:5px 12px;font-size:12px;color:var(--tree-secondary);background:transparent;white-space:nowrap}.tree-toolbar button[aria-pressed=true]{background:var(--tree-soft);color:inherit}.tree-toolbar .tree-refresh{font-size:11px;color:var(--tree-muted);padding:5px 8px}.tree-toolbar .tree-mobile-toggle{display:none}
10
+ .tree-columns{display:grid;grid-template-columns:258px minmax(0,1fr);flex:1;min-height:0;position:relative}.tree-pane{display:flex;flex-direction:column;min-height:0;padding:18px 12px 16px;border-right:1px solid var(--tree-line);background:var(--tree-bg)}.tree-search{display:block;position:relative;margin-bottom:14px}.tree-search>span{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}.tree-search input{width:100%;font-size:12px;border:0;background:var(--tree-soft);padding:8px 10px}.tree-list{flex:1;min-height:0;overflow:auto}.tree-row{display:flex;align-items:center;min-height:35px;margin:2px 0;border-radius:8px;padding-right:6px;gap:1px}.tree-row:hover{background:var(--tree-soft)}.tree-row.selected{background:color-mix(in srgb,var(--tree-accent) 12%,var(--tree-bg))}.tree-row.selected .tree-node-name{font-weight:600}.arcfold-tree .tree-disclosure{font-size:15px;width:20px;padding:0;flex:none;min-height:28px;border:0;background:transparent;color:var(--tree-muted)}.tree-disclosure-placeholder{width:20px;flex:none}.arcfold-tree .tree-node{display:flex;align-items:center;gap:7px;flex:1;min-width:0;text-align:left;border:0;background:transparent;padding:5px 2px;min-height:35px;font-size:12px;font-weight:400}.tree-node-name{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.tree-node small{white-space:nowrap;font-size:10px}.tree-dot{width:7px;height:7px;flex:none;display:inline-block;border-radius:50%;background:#54816b}.tree-dot.paused{background:#b88238}.tree-dot.completed{background:#868e96}.tree-dot.abandoned{background:var(--tree-danger)}.tree-legend{display:flex;flex-wrap:wrap;gap:8px 11px;padding:14px 4px 0;margin-top:10px;border-top:1px solid var(--tree-faint);font-size:10px;color:var(--tree-muted)}.tree-legend span{display:flex;align-items:center;gap:5px}.tree-legend .tree-dot{width:6px;height:6px}.tree-scrim{display:none}
11
+ .tree-detail-scroll{min-width:0;overflow:auto;padding:26px 30px 44px}.tree-detail{max-width:1040px;margin:auto;min-width:0}.tree-breadcrumbs{display:flex;align-items:center;flex-wrap:wrap;gap:5px 7px;margin-bottom:8px;color:var(--tree-muted);font-size:11px}.tree-breadcrumbs button{padding:0;border:0;min-height:0;font-size:11px;max-width:240px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:400;background:none}.tree-node-title>summary{list-style:none;display:flex;align-items:flex-start;gap:8px}.tree-node-title>summary::-webkit-details-marker{display:none}.tree-node-title>summary:after{content:'⌄';opacity:0;color:var(--tree-muted);padding-top:5px}.tree-node-title>summary:hover:after,.tree-node-title>summary:focus-visible:after,.tree-node-title[open]>summary:after{opacity:1}.tree-node-title h2{overflow-wrap:anywhere;letter-spacing:-.3px}.tree-node-status{display:flex;align-items:center;gap:6px;color:var(--tree-secondary);font-size:11px;margin-top:7px!important;margin-bottom:22px!important}.tree-panel{min-width:0;border:1px solid var(--tree-line);border-radius:12px;padding:17px 19px;background:var(--tree-bg)}.tree-panel>h3{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px}.tree-summary{margin-bottom:16px}.tree-summary-copy{font-size:13px;line-height:1.85;overflow-wrap:anywhere}.tree-summary-copy p+p{margin-top:8px}.tree-summary-more{margin-top:10px;font-size:12px}.tree-summary-more>summary{color:var(--tree-muted);font-size:11px}.tree-summary-more ul{margin:4px 0;padding-left:18px}.tree-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;align-items:start}.tree-item{border-top:1px solid var(--tree-faint)}.tree-panel>.tree-item:nth-child(2){border-top:0}.tree-item>summary{display:flex;align-items:flex-start;gap:12px;list-style:none;padding:13px 0;font-size:13px;line-height:1.75;overflow-wrap:anywhere}.tree-item>summary::-webkit-details-marker{display:none}.tree-item>summary>span{flex:1;min-width:0}.tree-item>summary:after{content:'›';font-size:18px;line-height:22px;color:var(--tree-muted);transform:rotate(0);transition:transform .15s}.tree-item[open]>summary:after{transform:rotate(90deg)}.tree-item[open]>summary{padding-bottom:9px}.tree-item>summary:hover{color:var(--tree-accent)}.tree-item-detail{font-size:12px;padding:0 0 16px}.tree-item-actions{display:flex;align-items:flex-start;gap:8px;flex-wrap:wrap;margin:2px 0 1px}.tree-item-actions button{font-size:12px;padding:6px 14px}.tree-command{display:inline-flex;flex-wrap:wrap;align-items:center;gap:6px;max-width:100%}.tree-command small{font-size:11px}.tree-method-description{background:var(--tree-soft);border-radius:8px;padding:12px;margin-top:12px;color:var(--tree-secondary)}.tree-method-description dl{display:grid;gap:7px;margin:0}.tree-method-description dl>div{display:grid;grid-template-columns:50px minmax(0,1fr);gap:9px}.tree-method-description dt{color:var(--tree-muted)}.tree-method-description dd{margin:0;overflow-wrap:anywhere;white-space:pre-wrap}.tree-evaluation-met{color:#54816b}.tree-evaluation-not_met{color:var(--tree-danger)}.tree-method-more{margin-top:10px}.tree-method-more>summary{color:var(--tree-muted)}.tree-method-more p{white-space:pre-wrap;margin-top:8px}.tree-method-more strong{display:block;margin-top:10px}.tree-editor,.tree-delete-confirm{margin:12px 0;padding:12px;border:1px solid var(--tree-line);border-radius:8px}.tree-delete-confirm p{margin-bottom:8px}.tree-delete-confirm>button{margin-left:8px}.tree-field{display:block;margin:8px 0;color:var(--tree-secondary)}.tree-field input,.tree-field select{display:block;width:100%;margin-top:4px}.arcfold-tree textarea{display:block;width:100%;min-height:90px;resize:vertical;margin:6px 0 12px}.arcfold-tree .tree-child{display:flex;align-items:center;gap:8px;width:100%;text-align:left;font-size:12px;padding:9px 0;border:0;border-top:1px solid var(--tree-faint);border-radius:0;background:none;font-weight:400}.tree-child>span:nth-child(2){flex:1;overflow-wrap:anywhere}.tree-child>span:last-child{color:var(--tree-muted)}.tree-child:nth-child(2){border-top:0}
12
+ .tree-page{overflow:auto;flex:1;min-height:0;padding:32px 40px 48px}.tree-page-heading{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:28px}.tree-page-heading p{font-size:12px;color:var(--tree-muted);margin-top:6px}.tree-metric-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}.tree-metric-card{padding:19px 20px}.tree-metric-card h3{color:inherit;font-size:13px}.tree-metric-value{font-size:30px;font-weight:500;line-height:1.5;margin-top:20px}.tree-metric-value small{font-size:12px;margin-left:7px;color:var(--tree-secondary)}.tree-metric-card p{font-size:11px;margin-top:7px}.tree-chart{width:100%;height:100px;margin-top:24px;overflow:visible}.tree-chart polyline{fill:none;stroke:var(--tree-accent);stroke-width:2}.tree-chart circle{fill:var(--tree-accent)}.tree-chart-base{stroke:var(--tree-line);fill:none}.tree-chart-empty{height:100px;display:flex;align-items:center;justify-content:center;font-size:11px;color:var(--tree-muted);margin-top:24px}.tree-outcomes{margin-top:16px}.tree-outcome-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:20px}.tree-outcome-grid span{display:block;color:var(--tree-secondary);font-size:12px}.tree-outcome-grid strong{display:block;font-size:26px;font-weight:500;margin-top:13px}.tree-reports>p{color:var(--tree-muted);font-size:12px;margin:8px 0 24px}.tree-reports article{border:1px solid var(--tree-line);border-radius:12px;padding:16px;margin-top:12px}.tree-reports article>button{display:block;text-align:left;border:0;padding:0;background:transparent;font-size:13px}.tree-reports pre{padding:12px;background:var(--tree-soft);border-radius:8px}
13
+ .tree-dock{display:flex;gap:6px;flex-wrap:wrap;font-size:11px;min-width:0}.arcfold-tree .tree-chip{display:inline-flex;align-items:center;gap:7px;max-width:min(100%,360px);min-width:0;min-height:28px;border-radius:7px;font-size:11px;padding:4px 8px;text-align:left;font-weight:400}.tree-chip>span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tree-chip>small{white-space:nowrap;flex:none}.tree-detail-dialog{position:fixed;inset:8vh max(16px,10vw);z-index:1000;background:var(--tree-bg);overflow:auto;padding:24px;border:1px solid var(--tree-line);border-radius:12px;box-shadow:0 0 0 100vmax #0005}.tree-detail-dialog>button{float:right;margin-left:12px}.tree-attribution-range{width:100%;min-width:0;padding:12px;border:1px solid var(--tree-line);border-radius:8px;font-size:12px}.tree-attribution-range blockquote{white-space:pre-wrap;overflow-wrap:anywhere;margin:8px 0;padding-left:12px;border-left:2px solid var(--tree-line)}.tree-attribution-range select{max-width:100%}
14
+ @container(max-width:1000px){.tree-columns{grid-template-columns:230px minmax(0,1fr)}.tree-detail-scroll{padding:24px}.tree-detail-grid{grid-template-columns:1fr}.tree-metric-value small{display:block;margin-left:0}.tree-page{padding:28px 24px}}
15
+ @container(max-width:700px){.tree-toolbar{padding:0 12px;gap:4px}.tree-toolbar nav{gap:2px}.tree-toolbar button{padding:5px 8px;font-size:11px}.tree-toolbar .tree-mobile-toggle{display:block}.tree-toolbar .tree-refresh{padding:5px}.tree-columns{display:block;overflow:hidden}.tree-pane{position:absolute;inset:0 auto 0 0;width:min(290px,85%);z-index:3;visibility:hidden;transform:translateX(-100%);transition:transform .15s;background:var(--tree-bg)}.tree-drawer-open .tree-pane{transform:none;visibility:visible;box-shadow:12px 0 24px #0001}.tree-drawer-open .tree-scrim{display:block;position:absolute;inset:0;border:0;border-radius:0;max-width:none;width:100%;height:100%;padding:0;background:#0003;color:transparent;font-size:0;z-index:2}.tree-detail-scroll{height:100%;padding:24px 16px 36px}.tree-detail h2{font-size:19px}.tree-panel{padding:15px 16px}.tree-breadcrumbs button{max-width:180px}.tree-page{padding:24px 16px}.tree-page-heading{align-items:flex-start}.tree-page-heading h2{font-size:19px}.tree-metric-grid{grid-template-columns:1fr}.tree-metric-value small{display:inline;margin-left:7px}.tree-chart-empty{height:70px;margin-top:12px}.tree-outcome-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.tree-detail-dialog{inset:16px;padding:18px}.tree-detail-dialog .tree-detail-grid{grid-template-columns:1fr}}
16
+ @media(pointer:coarse){.arcfold-tree .tree-item-actions button,.arcfold-tree .tree-command button{min-height:44px}.tree-item>summary{min-height:44px}}
17
+ @media(prefers-reduced-motion:reduce){.arcfold-tree *{transition:none!important}}
18
+
19
+ .tree-report-list .tree-report{border:0;border-top:1px solid var(--tree-faint);border-radius:0;padding:0;margin:0}.tree-report-list .tree-report:first-child{border:0}.tree-report-list .tree-report-toggle{display:flex;align-items:center;justify-content:space-between;width:100%;gap:12px;padding:14px 0;font-weight:400}.tree-report-toggle>span:last-child{color:var(--tree-muted)}.tree-report-detail{padding-bottom:16px}.tree-report-evidence{display:flex;align-items:center;gap:8px;margin:14px 0;font-size:12px;color:var(--tree-secondary)}.tree-report-detail .tree-method-description{margin-top:0}
@@ -0,0 +1,278 @@
1
+ import React, { useEffect, useState, useSyncExternalStore } from 'react';
2
+ const h = React.createElement;
3
+ const statuses = { active: '进行中', paused: '暂停', completed: '已完成', abandoned: '已放弃' };
4
+ const nodeStatus = node => (statuses[node.status] ?? node.status) + (node.statusOrigin === 'cascade' ? '(随上级)' : node.status === 'completed' && node.statusOrigin === 'model' ? '(助手判断)' : '');
5
+ const validation = { unverified: '尚待验证', supported: '有证据支持', contradicted: '不成立', changed: '已变化', invalid: '不成立' };
6
+ const categories = { not_understood: '没听懂', misread: '误解', restart: '需要重新对齐', redo: '要求重做', omitted: '漏做', repeated: '重复要求', overdone: '做过头', misreported: '说的与做的不一致', approved: '明确认可', adopted_proposal: '采纳提议', confirmed_inference: '证实推断', proceeded: '接受并推进' };
7
+ const sourceNames = { user_expressed: '用户表达', assistant_inferred: '助手推断', unknown: '尚不清楚' };
8
+ const useSource = source => useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot);
9
+ function useRead(source, endpoint, payload, refresh = 0) {
10
+ const [value, setValue] = useState({ loading: true });
11
+ const key = JSON.stringify([endpoint, payload, source.getSnapshot?.().epoch ?? 0]);
12
+ const refreshRevision = source.getSnapshot?.().refreshRevision ?? 0;
13
+ // Refresh the same object without unmounting an open item or unsent edit.
14
+ useEffect(() => { let live = true; if (!payload) return;
15
+ source.read(endpoint, payload).then(data => { if (live) setValue({ source, key, data }); }, error => { if (live) setValue({ source, key, error: error.message }); });
16
+ return () => { live = false; };
17
+ }, [source, endpoint, key, refresh, refreshRevision]);
18
+ return value.source === source && value.key === key && payload ? value : { loading: true };
19
+ }
20
+ const button = (label, onClick, props = {}) => h('button', { type: 'button', onClick, ...props }, label);
21
+ const field = (label, value, change, props = {}) => h('label', { className: 'tree-field' }, label, h('input', { value, onChange: e => change(e.target.value), ...props }));
22
+ const select = (label, value, change, options, props = {}) => h('label', { className: 'tree-field' }, label, h('select', { 'aria-label': label, value, onChange: e => change(e.target.value), ...props }, options.map(([id, label]) => h('option', { key: id, value: id }, label))));
23
+ function ReadState({ value, children }) { return value.error ? h('p', { role: 'alert' }, value.error) : value.loading ? h('p', { role: 'status' }, '正在读取…') : children(value.data); }
24
+ function Command({ source, endpoint, payload, children, onDone, disabled = false, className, title }) {
25
+ const state = useSource(source), [id, setId] = useState(null), op = state.operations[id];
26
+ const waiting = op && ['pending', 'unknown', 'not_found', 'partial', 'prepared', 'spawn_intent'].includes(op.state);
27
+ const run = async () => { const requestId = globalThis.crypto.randomUUID(); setId(requestId);
28
+ try { const receipt = await source.act(endpoint, typeof payload === 'function' ? payload() : payload, requestId); if (receipt.state === 'success') onDone?.(receipt.value); } catch {} };
29
+ return h('span', { className: 'tree-command' }, button(children, run, { disabled: disabled || waiting, className, title }), op && h('small', { role: op.state === 'conflict' ? 'alert' : 'status' }, op.state === 'success' ? '已保存' : op.message ?? '结果尚待确认'), waiting && op.state !== 'pending' && button('核对操作结果', () => source.readback(id).catch(() => {})));
30
+ }
31
+ function StatusDot({ status }) { return h('i', { className: 'tree-dot ' + status, 'aria-hidden': true }); }
32
+ function NodeChip({ node, path, source }) {
33
+ const label = (path ?? [node]).map(n => n.name).join(' › ');
34
+ return button(h(React.Fragment, null, h(StatusDot, { status: node.status }), h('span', null, label), h('small', null, nodeStatus(node))), () => source.openDetails(node.id), { className: 'tree-chip', title: label + ' · ' + nodeStatus(node) });
35
+ }
36
+ function AttributionRange({ source, state }) {
37
+ const range = state.current.attribution, [editing, setEditing] = useState(false), [branch, setBranch] = useState('');
38
+ useEffect(() => setBranch(range?.selection.nodeId ?? ''), [range?.selection.revision]);
39
+ if (!range || !range.blocked && !range.selection.nodeId && !range.pending.length && !editing) return null;
40
+ const payload = pendingId => ({ nodeId: branch || null, expectedTreeRevision: state.treeRevision, expectedSelectionRevision: range.selection.revision, pendingId });
41
+ const options = [['', '整棵目标树'], ...orderedNodes(state.nodes).map(n => {
42
+ const path = [n.name]; let parent = state.nodes.find(p => p.id === n.parentId); const seen = new Set([n.id]);
43
+ while (parent && !seen.has(parent.id)) { seen.add(parent.id); path.unshift(parent.name); parent = state.nodes.find(p => p.id === parent.parentId); }
44
+ return [n.id, path.join(' › ')];
45
+ })];
46
+ return h('section', { className: 'tree-attribution-range', 'aria-label': '本聊天目标范围' },
47
+ range.blocked ? h('p', { role: 'alert' }, range.selectionMissing ? '此前选定的目标已不存在,请重新选择。' : `当前范围有 ${range.count} 个候选,超过一次可检查的 ${range.limit} 个。请选择一支目标后继续。`) : h('p', null, '本聊天只在这支目标下判断归属:' + (range.path.map(n => n.name).join(' › ') || '整棵目标树')),
48
+ h('small', null, '选定范围后,其他分支暂不参与本聊天的归属判断。原话会保留,选择前不调用模型。'),
49
+ (range.blocked || range.pending.length > 0 || editing) ? h(React.Fragment, null,
50
+ select('本聊天使用哪支目标', branch, setBranch, options),
51
+ h(Command, { source, endpoint: 'session.selectBranch', payload: () => payload(null), onDone: () => setEditing(false) }, '保存范围'),
52
+ range.pending.map(p => h('article', { key: p.id }, h('p', null, '等待继续的原话'), h('blockquote', null, p.text),
53
+ p.state === 'waiting' ? h(Command, { source, endpoint: 'session.selectBranch', payload: () => payload(p.id), onDone: () => setEditing(false) }, '选定并继续这条消息') : h('p', { role: 'status' }, p.state === 'queued' ? '已经排入对话,请勿重复发送。' : '继续请求已记录,发送状态待核对;不会自动重发。')))) : button('更改范围', () => { setBranch(range.selection.nodeId ?? ''); setEditing(true); }));
54
+ }
55
+ export function TreeDock({ source }) {
56
+ const state = useSource(source), [revision, setRevision] = useState(0), detail = useRead(source, 'node.details', state.detailsNodeId ? { nodeId: state.detailsNodeId } : null, revision + state.epoch);
57
+ return h('div', { className: 'arcfold-tree tree-dock', 'aria-label': '当前事项' }, h(AttributionRange, { source, state }), state.current.paths.map(path => path.length ? h(NodeChip, { key: path.at(-1).id, node: path.at(-1), path, source }) : null), state.error && h('small', { role: 'alert' }, state.error),
58
+ state.detailsNodeId && h('div', { role: 'dialog', 'aria-modal': true, 'aria-label': '事项详情', className: 'tree-detail-dialog' }, button('关闭详情', () => source.closeDetails()), h(ReadState, { value: detail }, data => h(NodeDetail, { key: data.node.id, source, detail: data, navigate: id => source.openDetails(id), onDone: () => { setRevision(v => v + 1); void source.refresh(); } }))));
59
+ }
60
+ function ItemEditor({ source, item, onDone, onCancel }) {
61
+ const [expectedVersion] = useState(item.current.version);
62
+ const [text, setText] = useState(typeof item.current.content === 'string' ? item.current.content : ''),
63
+ [short, setShort] = useState(item.method?.short ?? item.current.content), [long, setLong] = useState(item.method?.long), [reason, setReason] = useState('');
64
+ const replacement = () => item.kind === 'understanding' ? text : item.kind === 'supplement' ? short : { short, long };
65
+ return h('section', { className: 'tree-editor' }, item.kind === 'understanding' ? h('label', null, '理解内容', h('textarea', { value: text, onChange: e => setText(e.target.value), maxLength: 2000 })) : h(React.Fragment, null,
66
+ Object.entries({ situation: '何时适用', action: '怎样做', basis: '依据', notApplicable: '何时不适用' }).map(([key, label]) => field(label, short?.[key] ?? '', value => setShort({ ...short, [key]: value }), { key, maxLength: 200 })),
67
+ long && h('details', null, h('summary', null, '详细方法文字'), Object.entries({ name: '名称', description: '说明', whenToUse: '适用情境', steps: '步骤', pitfalls: '注意点', verify: '如何检查' }).map(([key, label]) => h('label', { key }, label, h('textarea', { value: long[key] ?? '', onChange: e => setLong({ ...long, [key]: e.target.value }) }))))),
68
+ field('修改理由', reason, setReason), h('div', { className: 'tree-item-actions' }, h(Command, { source, endpoint: 'item.edit', className: 'tree-primary', disabled: !reason.trim() || item.kind === 'understanding' && !text.trim(), payload: () => ({ itemId: item.id, expectedVersion, replacement: replacement(), reason }), onDone }, '保存修改'), button('取消', onCancel)));
69
+ }
70
+ function ItemCard({ source, item, relatedItems = [], onDone }) {
71
+ const [editing, setEditing] = useState(false), [deleting, setDeleting] = useState(false), current = item.current, method = item.method;
72
+ const evaluation = item.evaluations?.filter(e => e.version === current.version).at(-1), short = method?.short ?? current.content?.short ?? current.content;
73
+ const label = typeof current.content === 'string' ? current.content : method?.long?.name ?? short?.action ?? '查看内容';
74
+ const active = item.lifecycle === 'active';
75
+ const replacement = relatedItems.find(candidate => candidate.id === item.replacedBy);
76
+ const facts = method || typeof current.content !== 'string' ? [
77
+ ['何时用', short?.situation], ['怎样做', short?.action],
78
+ ['预期', method?.expectation?.text ?? '尚未设定'],
79
+ ['检验', evaluation ? ({ met: '达到预期', not_met: '未达到预期', unclear: '尚不清楚' }[evaluation.result] ?? '尚不清楚') + (evaluation.observed ? ' · ' + evaluation.observed : '') : '尚待检验'],
80
+ ] : [];
81
+ return h('details', { className: 'tree-item', 'data-item-id': item.id },
82
+ h('summary', null, h('span', null, label), !active && h('small', null, ({ invalid: '已更正', changed: '已变化', draft: '待采用' })[item.lifecycle] ?? '已停止采用')),
83
+ h('div', { className: 'tree-item-detail' },
84
+ h('div', { className: 'tree-item-actions' },
85
+ item.kind === 'understanding' && current.sourceKind === 'assistant_inferred' && active && !item.stale && h(Command, { source, endpoint: 'item.confirm', className: 'tree-primary', payload: { itemId: item.id, expectedVersion: current.version }, onDone }, '确认'),
86
+ method && active && (item.hooks ?? []).map(hook => h(Command, { key: hook.id, source, endpoint: 'hook.disable', disabled: !hook.enabled || item.stale, title: item.hooks.length > 1 ? hook.message : undefined, payload: { methodId: item.id, hookId: hook.hookId, expectedVersion: current.version }, onDone }, hook.enabled ? '停用' : '已停用')),
87
+ active && button('修改', () => { setEditing(true); setDeleting(false); }, { disabled: editing }),
88
+ ['active', 'draft'].includes(item.lifecycle) && button('删除', () => { setDeleting(true); setEditing(false); }, { className: 'tree-danger', disabled: deleting })),
89
+ deleting && h('div', { className: 'tree-delete-confirm' }, h('p', null, '删除这条内容?'), h(Command, { source, endpoint: 'item.withdraw', className: 'tree-danger', payload: { itemId: item.id, expectedVersion: current.version, reason: '用户在目标树中删除此条内容' }, onDone }, '确认删除'), button('取消', () => setDeleting(false))),
90
+ editing && h(ItemEditor, { source, item, onDone: () => { setEditing(false); onDone(); }, onCancel: () => setEditing(false) }),
91
+ facts.length > 0 && h('div', { className: 'tree-method-description' }, h('dl', null, facts.map(([name, value]) => h('div', { key: name }, h('dt', null, name), h('dd', { className: name === '检验' && evaluation ? 'tree-evaluation-' + evaluation.result : undefined }, value || '尚未填写')))),
92
+ (short?.basis || short?.notApplicable || method?.long || item.scripts?.length) && h('details', { className: 'tree-method-more' }, h('summary', null, '完整说明'),
93
+ short?.basis && h('p', null, short.basis), short?.notApplicable && h('p', null, '不适用:' + short.notApplicable),
94
+ method?.long && Object.entries({ description: '说明', whenToUse: '适用情境', steps: '步骤', pitfalls: '注意点', verify: '如何检查' }).filter(([key]) => method.long[key]).map(([key, name]) => h('div', { key }, h('strong', null, name), h('p', null, method.long[key]))),
95
+ (item.scripts ?? []).filter(script => script.usage).map(script => h('p', { key: script.id }, script.usage)))),
96
+ item.stale && h('p', { role: 'status', className: 'tree-muted' }, item.sourceReview?.status === 'needs_evidence' ? '归属已变化,现有证据不足,暂不使用:' + item.sourceReview.reason : '归属已变化,此条暂不使用;请核对内容。'),
97
+ replacement && ['invalid', 'changed'].includes(item.lifecycle) && h('details', null, h('summary', null, '撤销自动更正'), h('p', null, '恢复原理解,并撤回系统生成的替代理解。'),
98
+ h(Command, { source, endpoint: 'item.undoCorrection', payload: { itemId: item.id, expectedRevision: item.revision, replacementId: replacement.id, expectedReplacementRevision: replacement.revision }, onDone }, '确认撤销更正'))));
99
+ }
100
+ function NodeDetail({ source, detail, onDone, navigate = id => source.select(id) }) {
101
+ const { node, summary } = detail, [name, setName] = useState(node.name), [nameRevision, setNameRevision] = useState(node.revision);
102
+ const items = detail.items.filter(item => item.lifecycle !== 'withdrawn');
103
+ return h('section', { className: 'tree-detail' },
104
+ h('nav', { className: 'tree-breadcrumbs', 'aria-label': '目标路径' }, detail.path.slice(0, -1).map(n => h(React.Fragment, { key: n.id }, button(n.name, () => navigate(n.id), { title: n.name }), h('span', { 'aria-hidden': true }, '›')))),
105
+ h('details', { className: 'tree-node-title' }, h('summary', { onClick: e => { if (!e.currentTarget.parentElement.open) { setName(node.name); setNameRevision(node.revision); } } }, h('h2', null, node.name)),
106
+ h('div', { className: 'tree-editor' }, field('事项名称', name, setName, { maxLength: 80 }), h(Command, { source, endpoint: 'node.rename', className: 'tree-primary', disabled: !name.trim() || name === node.name, payload: () => ({ nodeId: node.id, name, expectedRevision: nameRevision }), onDone }, '保存名称'))),
107
+ h('p', { className: 'tree-node-status' }, h(StatusDot, { status: node.status }), nodeStatus(node)),
108
+ h('section', { className: 'tree-panel tree-summary' }, h('h3', null, '当前摘要'), summary ? h('div', { className: 'tree-summary-copy' },
109
+ h('p', null, summary.oneLine), summary.body.status && summary.body.status !== summary.oneLine && h('p', null, summary.body.status),
110
+ h('details', { className: 'tree-summary-more' }, h('summary', null, '展开完整摘要'),
111
+ summary.body.outputs.length > 0 && h(React.Fragment, null, h('h4', null, '已有产出'), h('ul', null, summary.body.outputs.map((text, i) => h('li', { key: i }, text)))),
112
+ summary.body.decisions.length > 0 && h(React.Fragment, null, h('h4', null, '已作出的决定'), h('ul', null, summary.body.decisions.map((decision, i) => h('li', { key: i }, decision.what)))),
113
+ summary.body.next && h('p', null, '下一步:' + summary.body.next), summary.body.pending.length > 0 && h(React.Fragment, null, h('h4', null, '待确认'), h('ul', null, summary.body.pending.map((text, i) => h('li', { key: i }, text)))))) : h('p', { className: 'tree-empty' }, '还没有整理出的摘要。')),
114
+ h('div', { className: 'tree-detail-grid' },
115
+ ...[['理解', items.filter(item => item.kind === 'understanding')], ['方法', items.filter(item => item.kind !== 'understanding')]].map(([label, entries]) => h('section', { className: 'tree-panel', key: label }, h('h3', null, label, h('small', null, entries.length + ' 条')), entries.length ? entries.map(item => h(ItemCard, { key: item.id, source, item, relatedItems: detail.items, onDone })) : h('p', { className: 'tree-empty' }, label === '理解' ? '还没有理解。' : '还没有方法。'))),
116
+ h('section', { className: 'tree-panel' }, h('h3', null, '子节点', h('small', null, detail.children.length ? detail.children.length + ' 个' : '无')), detail.children.length ? detail.children.map(child => button(h(React.Fragment, null, h(StatusDot, { status: child.status }), h('span', null, child.name), h('span', { 'aria-hidden': true }, '›')), () => navigate(child.id), { key: child.id, className: 'tree-child', title: child.name })) : h('p', { className: 'tree-empty' }, '还没有子节点。')),
117
+ h('section', { className: 'tree-panel' }, h('h3', null, '扩大范围记录', h('small', null, detail.promotionHistory.length + ' 条')), detail.promotionHistory.length ? detail.promotionHistory.map(row => h('p', { key: row.itemId }, row.reason ?? '此条内容已扩大适用范围。')) : h('p', { className: 'tree-empty' }, '暂无扩大范围记录。'))));
118
+ }
119
+ function Parameter({ source, node, parameter, onDone }) {
120
+ const [value, setValue] = useState(String(parameter.value)), days = parameter.name === 'retentionDays';
121
+ return h('section', null, field(days ? '结束后保留在候选中的天数' : '离开多久后整理(小时)', value, setValue, { type: 'number', min: 1, max: days ? 365 : 168 }), h('small', null, parameter.local?.actor.kind === 'user' ? '由你设置,后台不会覆盖。' : '当前值来自默认设置或上级事项。'), h(Command, { source, endpoint: 'parameters.set', payload: () => ({ nodeId: node.id, name: parameter.name, value: Number(value), expectedRevision: parameter.local?.revision ?? 0 }), onDone }, '保存设置'));
122
+ }
123
+ const metricCards = [
124
+ { title: '默契', label: '次需要重新对齐', count: o => o.rates.understandingProblems.numerator },
125
+ { title: '可靠', label: '次要求重做', count: o => o.counts.redo },
126
+ { title: '精进', label: '次重复纠正', count: o => o.rates.recurrence.numerator },
127
+ ];
128
+ function Metrics({ source, refresh }) {
129
+ const value = useRead(source, 'metrics.read', {}, refresh);
130
+ return h(ReadState, { value }, data => {
131
+ const current = data.observations, comparable = data.comparison?.comparable && data.windows?.length === 2 && data.windows[1].fromOrdinal === data.fromOrdinal && data.windows[1].toOrdinal === data.toOrdinal, previous = comparable ? data.windows[0].observations : null;
132
+ const growth = current.growth, hasData = current.logicalTurns > 0;
133
+ return h('section', { className: 'tree-page tree-metrics', 'aria-label': '指标' },
134
+ h('header', { className: 'tree-page-heading' }, h('div', null, h('h2', null, '三个特征'), h('p', null, '一起做事的现状与变化')), h('small', null, hasData ? '最近 ' + current.logicalTurns + ' 轮对话' : '暂无对话记录')),
135
+ h('div', { className: 'tree-metric-grid' }, metricCards.map(metric => {
136
+ const now = hasData ? metric.count(current) : null, points = comparable ? data.windows.map(w => metric.count(w.observations)) : [];
137
+ const max = Math.max(1, ...points), coordinates = points.map((n, i) => (10 + i * 240) + ',' + (68 - n / max * 50)).join(' ');
138
+ return h('article', { className: 'tree-panel tree-metric-card', key: metric.title }, h('h3', null, metric.title),
139
+ h('div', { className: 'tree-metric-value' }, now ?? '—', h('small', null, metric.label)),
140
+ h('p', { className: 'tree-muted' }, previous ? '前一观察阶段 ' + metric.count(previous) + ' 次(' + previous.logicalTurns + ' 轮)' : '暂无可比较的历史记录'),
141
+ points.length > 1 ? h('svg', { className: 'tree-chart', viewBox: '0 0 260 80', role: 'img', 'aria-label': '前后两个观察阶段:' + points.join('、') + '次' }, h('path', { d: 'M10 72H250', className: 'tree-chart-base' }), h('polyline', { points: coordinates }), points.map((n, i) => h('circle', { key: i, cx: 10 + i * 240, cy: 68 - n / max * 50, r: 3 }))) : h('div', { className: 'tree-chart-empty' }, hasData ? '积累更多记录后显示趋势' : '暂无数据'));
142
+ })),
143
+ h('section', { className: 'tree-panel tree-outcomes' }, h('h3', null, '自主的拓展'), h('div', { className: 'tree-outcome-grid' }, [
144
+ ['用户确认的变化', growth.confirmedUserChanges], ['新建目标', growth.byType.created],
145
+ ['作出取舍', growth.userStatusChoices.paused + growth.userStatusChoices.abandoned], ['确认完成', growth.userStatusChoices.completed],
146
+ ].map(([label, count]) => h('div', { key: label }, h('span', null, label), h('strong', null, hasData ? count : '—'))))));
147
+ });
148
+ }
149
+ function Reports({ source, refresh }) {
150
+ const [cursor, setCursor] = useState(null), [selected, setSelected] = useState(null), [includeEvidence, setEvidence] = useState(false), [preview, setPreview] = useState(null), [download, setDownload] = useState(null);
151
+ const value = useRead(source, 'reports.list', { ...(cursor ? { cursor } : {}), limit: 50 }, refresh);
152
+ useEffect(() => { let live = true; setPreview(null); setDownload(null); if (selected) source.read('reports.details', { reportId: selected.reportId, includeEvidence }).then(value => { if (live) setPreview(value); }).catch(error => { if (live) setPreview({ error: error.message }); }); return () => { live = false; }; }, [source, selected, includeEvidence]);
153
+ useEffect(() => () => { if (download) URL.revokeObjectURL(download.url); }, [download]);
154
+ const open = report => { setSelected(selected?.reportId === report.reportId ? null : report); setEvidence(false); };
155
+ return h('section', { className: 'tree-page tree-reports' },
156
+ h('header', { className: 'tree-page-heading' }, h('div', null, h('h2', null, '给开发者的报告'), h('p', null, '需要从系统本身改进的问题'))),
157
+ h(ReadState, { value }, data => h(React.Fragment, null,
158
+ data.reports.length ? h('div', { className: 'tree-panel tree-report-list' }, data.reports.map(report => h('article', { className: 'tree-report', key: report.reportId },
159
+ button(h(React.Fragment, null, h('span', null, report.category ?? '问题报告'), h('span', { 'aria-hidden': true }, selected?.reportId === report.reportId ? '⌄' : '›')), () => open(report), { className: 'tree-report-toggle', 'aria-expanded': selected?.reportId === report.reportId, 'aria-label': report.category ?? '问题报告' }),
160
+ selected?.reportId === report.reportId && h('div', { className: 'tree-report-detail' },
161
+ preview?.error ? h('p', { role: 'alert' }, preview.error) : !preview ? h('p', { role: 'status' }, '正在读取…') :
162
+ h('div', { className: 'tree-method-description' }, h('dl', null, [
163
+ ['当前状态', ({ unhandled: '待开发者处理', accepted: '已处理', rejected: '未采用', exported: '已导出' })[preview.status] ?? '等待核对'],
164
+ ['发生了什么', preview.problem], ['影响环节', preview.step], ['影响范围', preview.whyUniversal], ['改进建议', preview.suggestedFix],
165
+ ['相关材料', preview.includesEvidence ? preview.evidenceRefs.length + ' 条记录将随报告导出' : '未附原始证据'],
166
+ ].map(([label, text]) => h('div', { key: label }, h('dt', null, label), h('dd', null, text))))),
167
+ h('label', { className: 'tree-report-evidence' }, h('input', { type: 'checkbox', checked: includeEvidence, onChange: e => setEvidence(e.target.checked) }), '附上原始证据'),
168
+ h('div', { className: 'tree-item-actions' }, h(Command, { source, endpoint: 'reports.export', disabled: !preview || Boolean(preview.error), payload: () => ({ reportIds: [selected.reportId], includeEvidence, expectedRevisions: { [selected.reportId]: preview.revision } }), onDone: receipt => { const artifact = receipt.result; setDownload({ url: URL.createObjectURL(new Blob([artifact.content], { type: artifact.mimeType })), filename: artifact.filename }); } }, '生成本地下载'),
169
+ download && h('a', { href: download.url, download: download.filename }, '下载报告')))))) : h('p', { className: 'tree-empty' }, '暂无报告。'),
170
+ data.nextCursor && button('下一页', () => { setSelected(null); setCursor(data.nextCursor); }), cursor && button('回到第一页', () => { setSelected(null); setCursor(null); }))));
171
+ }
172
+ export function TreeView({ source }) {
173
+ const state = useSource(source), [tab, setTab] = useState('tree'), [revision, setRevision] = useState(0), [drawer, setDrawer] = useState(false);
174
+ const selected = state.nodes.some(n => n.id === state.selection) ? state.selection : state.current.paths?.[0]?.at(-1)?.id ?? state.nodes.find(n => !n.parentId)?.id;
175
+ const detail = useRead(source, 'node.details', selected ? { nodeId: selected } : null, revision + state.epoch);
176
+ const reload = () => { setRevision(v => v + 1); void source.refresh(); }, navigate = id => { source.select(id); setDrawer(false); };
177
+ return h('div', { className: 'arcfold-tree tree-view' },
178
+ h('header', { className: 'tree-toolbar' }, h('nav', { 'aria-label': 'Arcfold 内容' },
179
+ button('选目标', () => setDrawer(!drawer), { className: 'tree-mobile-toggle', 'aria-expanded': drawer }),
180
+ ...[['tree', '目标树'], ['metrics', '指标'], ['reports', '给开发者的报告']].map(([id, label]) => button(label, () => { setTab(id); setDrawer(false); }, { key: id, 'aria-pressed': tab === id }))),
181
+ button('刷新', reload, { className: 'tree-refresh' })),
182
+ state.error && h('p', { role: 'alert' }, state.error), h(AttributionRange, { source, state }),
183
+ tab === 'reports' ? h(Reports, { source, refresh: revision }) : tab === 'metrics' ? h(Metrics, { source, refresh: revision }) :
184
+ h('div', { className: 'tree-columns' + (drawer ? ' tree-drawer-open' : ''), onKeyDown: e => { if (e.key === 'Escape') setDrawer(false); } },
185
+ drawer && button('关闭目标列表', () => setDrawer(false), { className: 'tree-scrim', tabIndex: -1 }),
186
+ h(TreeNavigation, { nodes: state.nodes, selected, navigate }),
187
+ h('div', { className: 'tree-detail-scroll' }, selected ? h(ReadState, { value: detail }, data => h(NodeDetail, { key: data.node.id, source, detail: data, onDone: reload, navigate })) : h('p', { className: 'tree-empty' }, state.loading ? '正在读取目标树…' : '还没有目标。'))));
188
+ }
189
+ function TreeNavigation({ nodes, selected, navigate }) {
190
+ const [query, setQuery] = useState(''), [collapsed, setCollapsed] = useState(new Set());
191
+ const byId = new Map(nodes.map(n => [n.id, n])), queryText = query.trim().toLocaleLowerCase();
192
+ const ancestors = id => { const result = [], seen = new Set([id]); let node = byId.get(id); while (node?.parentId && !seen.has(node.parentId)) { seen.add(node.parentId); result.push(node.parentId); node = byId.get(node.parentId); } return result; };
193
+ // Opening a node from a breadcrumb or child link also reveals its tree row.
194
+ useEffect(() => { setCollapsed(old => new Set([...old].filter(id => !ancestors(selected).includes(id)))); }, [selected]);
195
+ const matches = new Set(nodes.filter(n => n.name.toLocaleLowerCase().includes(queryText)).flatMap(n => [n.id, ...ancestors(n.id)]));
196
+ const visible = orderedNodes(nodes).filter(n => queryText ? matches.has(n.id) : !ancestors(n.id).some(id => collapsed.has(id)));
197
+ const toggle = id => setCollapsed(old => { const next = new Set(old); next.has(id) ? next.delete(id) : next.add(id); return next; });
198
+ const keyboard = (event, node, index) => {
199
+ let target;
200
+ if (event.key === 'ArrowDown') target = visible[Math.min(index + 1, visible.length - 1)];
201
+ if (event.key === 'ArrowUp') target = visible[Math.max(index - 1, 0)];
202
+ if (event.key === 'Home') target = visible[0]; if (event.key === 'End') target = visible.at(-1);
203
+ if (event.key === 'ArrowLeft') { if (!collapsed.has(node.id) && nodes.some(n => n.parentId === node.id) && !queryText) toggle(node.id); else target = byId.get(node.parentId); }
204
+ if (event.key === 'ArrowRight') { if (collapsed.has(node.id) && !queryText) toggle(node.id); else target = nodes.find(n => n.parentId === node.id); }
205
+ if (/^(Arrow|Home|End)/.test(event.key)) event.preventDefault();
206
+ if (target) { const buttons = event.currentTarget.closest('[role=tree]').querySelectorAll('[role=treeitem]'); [...buttons].find(b => b.dataset.nodeId === target.id)?.focus(); }
207
+ };
208
+ return h('aside', { className: 'tree-pane', 'aria-label': '目标树' },
209
+ h('label', { className: 'tree-search' }, h('span', null, '搜索目标'), h('input', { type: 'search', value: query, placeholder: '搜索目标…', onChange: e => setQuery(e.target.value) })),
210
+ h('div', { className: 'tree-list', role: 'tree', 'aria-label': '所有目标' }, visible.map((node, index) => {
211
+ const branch = nodes.some(n => n.parentId === node.id), expanded = queryText ? true : !collapsed.has(node.id), depth = ancestors(node.id).length;
212
+ return h('div', { key: node.id, className: 'tree-row' + (selected === node.id ? ' selected' : ''), style: { paddingInlineStart: Math.min(depth, 5) * 14 } },
213
+ branch ? button(expanded ? '⌄' : '›', () => toggle(node.id), { className: 'tree-disclosure', title: (expanded ? '收起' : '展开') + node.name, 'aria-label': (expanded ? '收起' : '展开') + node.name, 'aria-expanded': expanded, disabled: Boolean(queryText) }) : h('span', { className: 'tree-disclosure-placeholder' }),
214
+ button(h(React.Fragment, null, h(StatusDot, { status: node.status }), h('span', { className: 'tree-node-name' }, node.name), node.statusOrigin === 'cascade' && h('small', null, '随上级')), () => navigate(node.id), { role: 'treeitem', 'aria-level': depth + 1, 'aria-selected': selected === node.id, ...(branch ? { 'aria-expanded': expanded } : {}), className: 'tree-node', 'data-node-id': node.id, title: node.name + ' · ' + nodeStatus(node), onKeyDown: event => keyboard(event, node, index) }));
215
+ }), !visible.length && h('p', { className: 'tree-empty' }, '没有找到匹配的目标。')),
216
+ h('div', { className: 'tree-legend' }, Object.entries(statuses).map(([status, label]) => h('span', { key: status }, h(StatusDot, { status }), label))));
217
+ }
218
+ function orderedNodes(nodes) {
219
+ const result = [], seen = new Set(), visit = node => { if (seen.has(node.id)) return; seen.add(node.id); result.push(node); nodes.filter(n => n.parentId === node.id).forEach(visit); };
220
+ nodes.filter(n => !n.parentId || !nodes.some(parent => parent.id === n.parentId)).forEach(visit); nodes.forEach(visit); return result;
221
+ }
222
+ function CorrectTurn({ source, detail, state, onDone }) {
223
+ const [targets, setTargets] = useState(detail.turn.nodeIds), [create, setCreate] = useState(false), [name, setName] = useState(''), [parent, setParent] = useState(state.nodes.find(n => !n.parentId)?.id ?? '');
224
+ return h('details', null, h('summary', null, '纠正本轮归属'), state.nodes.map(n => h('label', { key: n.id, className: 'tree-check' }, h('input', { type: 'checkbox', checked: targets.includes(n.id), onChange: e => setTargets(e.target.checked ? [...targets, n.id] : targets.filter(id => id !== n.id)) }), n.name)),
225
+ h('label', null, h('input', { type: 'checkbox', checked: create, onChange: e => setCreate(e.target.checked) }), '新建事项或下级事项'), create && h(React.Fragment, null, field('新事项名称', name, setName, { maxLength: 80 }), select('上级事项', parent, setParent, state.nodes.map(n => [n.id, n.name]))),
226
+ h(Command, { source, endpoint: 'turn.correct', disabled: !targets.length && !create || create && !name.trim(), payload: () => ({ turnId: detail.turn.id, nodeIds: targets, expectedAssignmentRevision: detail.turn.assignmentRevision, ...(create ? { create: { name, parentId: parent, expectedTreeRevision: state.treeRevision } } : {}) }), onDone }, '移动本轮经历'));
227
+ }
228
+ function Restore({ source, turnId }) {
229
+ const [open, setOpen] = useState(false), [preview, setPreview] = useState(null), [paths, setPaths] = useState([]), [selected, setSelected] = useState(null);
230
+ const list = useRead(source, 'snapshots.list', open ? { logicalTurnId: turnId } : null);
231
+ if (!open) return button('恢复到这一轮之前', () => setOpen(true));
232
+ const showPreview = value => { setPreview(value); setPaths(value.changes.map(c => c.path)); };
233
+ const renderContent = (label, content) => h('section', { key: label },
234
+ h('strong', null, label),
235
+ h('pre', null, !content ? '不存在' : content.binary ? '二进制文件;' + content.size + ' 字节,校验值 ' + content.sha256 : content.text),
236
+ content?.truncated && h('small', null, '仅展示前 64 KiB;恢复操作针对完整文件。'));
237
+ const diffs = preview?.diffs.map(diff => h('article', { key: diff.path },
238
+ h('label', null, h('input', { type: 'checkbox', checked: paths.includes(diff.path), onChange: e => setPaths(e.target.checked ? [...paths, diff.path] : paths.filter(p => p !== diff.path)) }), diff.path),
239
+ h('div', { className: 'tree-diff' }, renderContent('当前内容', diff.current), renderContent('恢复为', diff.restoreTo))));
240
+ return h('section', { className: 'tree-restore' },
241
+ h('h4', null, '恢复前核对'), h('p', null, '恢复会覆盖所选文件。先检查变化,再确认;不会自动恢复。'),
242
+ h(ReadState, { value: list }, data => h(React.Fragment, null,
243
+ !data.snapshots.length && h('p', null, '这一轮没有可用快照。'),
244
+ data.snapshots.map(snapshot => h('article', { key: snapshot.id },
245
+ h('p', null, snapshot.coverage.state === 'full' ? '覆盖可保存的工作区内容' : '部分内容未覆盖'),
246
+ h('pre', null, JSON.stringify(snapshot.coverage.exclusions, null, 2)),
247
+ h(Command, { source, endpoint: 'snapshots.preview', payload: { snapshotId: snapshot.id, paths: null }, onDone: value => { setSelected(snapshot.id); showPreview(value); } }, '查看变化'))))),
248
+ preview && h(React.Fragment, null, diffs,
249
+ h(Command, { source, endpoint: 'snapshots.preview', disabled: !paths.length, payload: () => ({ snapshotId: selected, paths }), onDone: showPreview }, '更新所选文件的预览'),
250
+ h(Command, { source, endpoint: 'snapshots.restore', disabled: !preview.changes.length || JSON.stringify([...paths].sort()) !== JSON.stringify(preview.changes.map(c => c.path).sort()), payload: { previewId: preview.id, expectedHashes: preview.expectedCurrentHashes }, onDone: () => setPreview(null) }, '确认恢复这些文件')),
251
+ button('关闭', () => setOpen(false)));
252
+ }
253
+ function SignalCorrection({ source, signal, onDone }) {
254
+ const [category, setCategory] = useState(signal.category);
255
+ return h('span', null, select('纠正类别', category, setCategory, Object.entries(categories)), h(Command, { source, endpoint: 'signal.correct', disabled: category === signal.category, payload: () => ({ signalId: signal.id, category, expectedRevision: signal.revision }), onDone }, '保存类别'));
256
+ }
257
+ function BackgroundStatus({ jobs }) {
258
+ const states = jobs.filter(j => j.current !== false && !['main', 'attribution'].includes(j.role)).map(j => j.status);
259
+ const describeRejected = item => {
260
+ const name = ({ categories: '反馈分类', understandings: '理解', revisions: '理解更正', methods: '方法',
261
+ operations: '检验建议', verifications: '方法检验', probes: '待核事项', missed: '待核发现', formatFixes: '格式建议',
262
+ clusters: '共同问题判断', supplements: '补充做法', parameters: '设置建议', observations: '对话观察', proposals: '新建议' })[item.item.split(/[.\[]/)[0]] ?? '后台内容';
263
+ const reason = ({ OUTPUT_REFERENCE: '引用不在本次材料中', OUTPUT_CURRENT_SOURCE: '引用不是本轮用户原话',
264
+ OUTPUT_PROTECTED: '内容受到用户保护', USER_SOURCE_REQUIRED: '缺少用户原话依据', OUTPUT_VERSION: '引用的内容版本已变化',
265
+ INVALID_OUTPUT: '内容格式不完整', CORRECTION_LIMIT: '本轮只保存一条纠正' })[item.code] ?? '来源、范围或内容格式未通过检查';
266
+ return `${name}:${reason}`;
267
+ };
268
+ return h(React.Fragment, null,
269
+ states.some(s => ['prepared', 'submitted', 'result_received', 'validated'].includes(s)) ? h('p', { role: 'status' }, '后台正在补学或检验本轮内容。') :
270
+ states.some(s => ['queued', 'pending'].includes(s)) && h('p', { role: 'status' }, '本轮内容等待后台处理。'),
271
+ states.includes('unknown') && h('p', { role: 'status' }, '本轮后台处理结果尚不确定,系统未重复发送。'),
272
+ states.some(s => ['failed', 'invalid_output'].includes(s)) && h('p', { role: 'status' }, '本轮部分后台学习或检验未完成,失败记录已保留。'),
273
+ jobs.some(j => j.current !== false && j.rejectedItems?.length) && h(React.Fragment, null,
274
+ h('p', { role: 'status' }, '本轮部分后台内容未通过检查,其他合格内容已保留。'),
275
+ h('details', null, h('summary', null, '查看未采用的内容'), jobs.filter(j => j.current !== false && j.rejectedItems?.length).flatMap(j =>
276
+ j.rejectedItems.map((item, index) => h('p', { key: j.id + ':' + index }, describeRejected(item)))))));
277
+ }
278
+ export function TreeTurnTail() { return null; }
@@ -0,0 +1,123 @@
1
+ import { currentMainAgent } from './main-session.mjs';
2
+ import { browseScope } from './browse-scope.mjs';
3
+ import { TypertRemoteService, RemoteError } from '@deepseek-ai/dsh-typert-protocol';
4
+ const NAMESPACE = 'arcfoldUnderstandings', SERVICE = 'arcfoldUnderstandingWeb';
5
+ const ENDPOINTS = ['list', 'details', 'decide', 'correct'];
6
+ const reject = (code, message) => { throw new RemoteError(code, message, {}); };
7
+ function shape(value, allowed, required = allowed) {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
9
+ Object.keys(value).some(key => !allowed.includes(key)) || required.some(key => !Object.hasOwn(value, key)))
10
+ reject('invalid_understanding_request', '理解操作包含缺失或未允许的字段。');
11
+ }
12
+ function id(value) {
13
+ if (typeof value !== 'string' || !value.trim() || value.length > 512 || value.includes('\0'))
14
+ reject('invalid_understanding_request', '请求身份无效。');
15
+ }
16
+ function trusted(connection, signal, peer) {
17
+ if (!peer || peer !== connection.browserPeer || peer.principal?.kind !== 'owner' ||
18
+ peer.interactions !== 'answer' || peer.signal?.aborted !== false || signal?.aborted !== false)
19
+ reject('trusted_browser_required', '此操作只接受当前已认证网页的请求。');
20
+ }
21
+ const errors = {
22
+ invalid_browse_scope: '请选择全部积累或与当前聊天相关的内容。',
23
+ revision_conflict: '所查看的版本已变化,请重新打开后决定。',
24
+ understanding_session_denied: '请打开当前的 Arcfold 主交流。',
25
+ understanding_not_found: '当前交流中无法查看这条理解。',
26
+ source_scope_denied: '这条来源不属于所查看的理解。',
27
+ source_unavailable: '这条来源已不可用,请刷新。',
28
+ understanding_unavailable: '当前版本不能执行这项操作,请刷新。',
29
+ understanding_identity_conflict: '这个提交编号对应另一份内容,未重复发送。',
30
+ understanding_command_changed: '这项操作已经变化,请刷新查看记录。',
31
+ };
32
+ // Only coalesce simultaneous invocations. The durable service owns replay rules.
33
+ export function createUnderstandingHandler({ understandings, agents, agentPresets, connection, prompt, inflight = new Map() }) {
34
+ const main = (sessionId, expected) => {
35
+ const agent = currentMainAgent({ agents, agentPresets }, sessionId);
36
+ if (!agent || expected && agent !== expected)
37
+ reject('understanding_session_denied', errors.understanding_session_denied);
38
+ return agent;
39
+ };
40
+ return async (endpoint, payload, signal, peer) => {
41
+ try {
42
+ trusted(connection, signal, peer);
43
+ if (!understandings) reject('understanding_unavailable', '当前部署尚未接通理解管理。');
44
+ if (endpoint === 'list') {
45
+ shape(payload, ['sessionId', 'offset', 'limit', 'viewScope'], ['sessionId']); id(payload.sessionId); browseScope(payload.viewScope);
46
+ if (payload.viewScope === undefined) main(payload.sessionId);
47
+ if (payload.offset !== undefined && (!Number.isSafeInteger(payload.offset) || payload.offset < 0) ||
48
+ payload.limit !== undefined && (!Number.isSafeInteger(payload.limit) || payload.limit < 1 || payload.limit > 100))
49
+ reject('invalid_understanding_request', '分页范围无效。');
50
+ return { ...understandings.list(payload), canCorrect: currentMainAgent({ agents, agentPresets }, payload.sessionId) !== null };
51
+ }
52
+ if (endpoint === 'details') {
53
+ shape(payload, ['sessionId', 'understandingId', 'viewScope'], ['sessionId', 'understandingId']); id(payload.sessionId); id(payload.understandingId); browseScope(payload.viewScope);
54
+ if (payload.viewScope === undefined) main(payload.sessionId);
55
+ const detail = understandings.details(payload);
56
+ return currentMainAgent({ agents, agentPresets }, payload.sessionId) ? detail : { ...detail,
57
+ controls: { ...detail.controls, stopAllowed: false, correctionAllowed: false, sourceControlAllowed: false } };
58
+ }
59
+ const common = ['sessionId', 'understandingId', 'expectedRevision', 'requestId'];
60
+ if (endpoint === 'correct') shape(payload, [...common, 'text']);
61
+ else if (endpoint === 'decide') shape(payload, [...common, 'action', 'sourceRef'], [...common, 'action']);
62
+ else reject('understanding_endpoint_not_found', '不存在的理解操作。');
63
+ for (const name of ['sessionId', 'understandingId', 'requestId']) id(payload[name]);
64
+ const agent = main(payload.sessionId);
65
+ if (!Number.isSafeInteger(payload.expectedRevision) || payload.expectedRevision < 1)
66
+ reject('invalid_understanding_request', '操作必须绑定已查看的版本。');
67
+ const request = { ...payload, userIdentity: 'browser:' + peer.id };
68
+ if (endpoint === 'decide') {
69
+ if (!['stop-understanding', 'withdraw-source'].includes(payload.action)) reject('invalid_understanding_request', '理解操作无效。');
70
+ if (payload.action === 'withdraw-source') {
71
+ shape(payload.sourceRef, ['id', 'revision', 'expectedControlRevision']); id(payload.sourceRef.id);
72
+ const { revision, expectedControlRevision } = payload.sourceRef;
73
+ if (!Number.isSafeInteger(revision) || revision < 1 || expectedControlRevision !== null &&
74
+ (!Number.isSafeInteger(expectedControlRevision) || expectedControlRevision < 1)) reject('invalid_understanding_request', '来源版本无效。');
75
+ } else if (Object.hasOwn(payload, 'sourceRef')) reject('invalid_understanding_request', '停止理解不接受来源参数。');
76
+ return understandings.decide(request);
77
+ }
78
+ if (typeof payload.text !== 'string' || !payload.text.trim() || payload.text.length > 16000 || payload.text.includes('\0'))
79
+ reject('invalid_correction_text', '请填写不超过 16000 字符的完整替换内容。');
80
+ const bound = understandings.bindCorrection(request), key = bound.command.id;
81
+ if (inflight.has(key)) return await inflight.get(key);
82
+ if (!bound.created) return bound;
83
+ const pending = Promise.resolve().then(async () => {
84
+ let entered = false, state;
85
+ try {
86
+ trusted(connection, signal, peer); main(payload.sessionId, agent); entered = true;
87
+ const result = await prompt({ sessionId: payload.sessionId, requestId: payload.requestId,
88
+ mode: 'steer', content: [{ type: 'text', text: payload.text }] }, signal);
89
+ state = result?.accepted === true ? 'accepted' : 'unknown';
90
+ } catch { state = entered ? 'unknown' : 'failed'; }
91
+ const command = understandings.recordCorrectionAdmission({ commandId: key, state });
92
+ return { ...bound, command };
93
+ });
94
+ inflight.set(key, pending);
95
+ try { return await pending; } finally { if (inflight.get(key) === pending) inflight.delete(key); }
96
+ } catch (error) {
97
+ if (error instanceof RemoteError) throw error;
98
+ const code = Object.hasOwn(errors, error?.code) ? error.code : 'understanding_request_failed';
99
+ throw new RemoteError(code, errors[code] ?? '操作未确认,请刷新查看原始记录。未自动重试。', {});
100
+ }
101
+ };
102
+ }
103
+ class UnderstandingWeb extends TypertRemoteService {
104
+ inflight = new Map();
105
+ constructor(ctx) { super(ctx, SERVICE, { namespace: NAMESPACE }); }
106
+ dispatch(method, request) {
107
+ const { signal, peer } = this.ctx.invocation ?? {}, controller = this.ctx.sessionController;
108
+ return createUnderstandingHandler({ understandings: this.ctx.arcfoldUnderstandings, agents: this.ctx.agents, agentPresets: this.ctx.agentPresets,
109
+ connection: this.ctx.connection, inflight: this.inflight,
110
+ prompt: (input, abort) => controller.prompt(input, abort) })(method, request, signal, peer);
111
+ }
112
+ list(value) { return this.dispatch('list', value); }
113
+ details(value) { return this.dispatch('details', value); }
114
+ decide(value) { return this.dispatch('decide', value); }
115
+ correct(value) { return this.dispatch('correct', value); }
116
+ }
117
+ export const understandingInvocations = ENDPOINTS.map(method => ({
118
+ id: `@arcfold/dsh-method-ui#${NAMESPACE}/${method}`, service: SERVICE, namespace: NAMESPACE, method,
119
+ invocation: { kind: 'direct' }, parameters: [{ name: 'request', wire: 'request', source: 'json', codec: { mode: 'src-json' } }], result: { mode: 'src-json' },
120
+ }));
121
+ export function installUnderstandingWeb(ctx) {
122
+ return ctx.inject(['arcfoldUnderstandings', 'agents', 'agentPresets', 'sessionController', 'connection'], owner => { new UnderstandingWeb(owner); });
123
+ }