@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
package/src/client.mjs ADDED
@@ -0,0 +1,138 @@
1
+ import { CollaborationView, CollaborationHero } from './CollaborationView.mjs';
2
+ import { createBrowseScopeSource } from './browse-source.mjs';
3
+ import { createOpportunitySource } from './opportunity-source.mjs';
4
+ import { opportunitiesZh, opportunitiesEn } from './opportunity-locales.mjs';
5
+ import { createImportSource } from './import-source.mjs';
6
+ import { importsZh, importsEn } from './import-locales.mjs';
7
+ import { createUnderstandingSource } from './understanding-source.mjs';
8
+ import { understandingZh, understandingEn } from './understanding-locales.mjs';
9
+ import { ModelSettings } from './ModelSettings.mjs';
10
+ import { createModelSettingsSource } from './model-settings-source.mjs';
11
+ import { modelsZh, modelsEn } from './model-settings-locales.mjs';
12
+ import { createGoalSource } from './goal-source.mjs';
13
+ import { goalZh, goalEn } from './goal-locales.mjs';
14
+ import { createMethodSource } from './method-source.mjs';
15
+ import { zh, en } from './locales.mjs';
16
+ import { installTreeClient } from './tree-client.mjs';
17
+ import { DesktopSetup } from './DesktopSetup.mjs';
18
+ import { DesktopModels } from './DesktopModels.mjs';
19
+ import { openDesktopWorkspace } from './desktop-workspace.mjs';
20
+ export const inject = ['slots', 'connection', 'locale', 'fileUpload'];
21
+ const NS = 'arcfold-methods';
22
+ export async function apply(ctx) {
23
+ let desktop;
24
+ try { desktop = await ctx.connection.rpc.call('/api', 'arcfoldDesktop/status', { args: { request: {} } }); } catch {}
25
+ if (desktop?.ok === true && ['needs_setup', 'configured', 'activation_failed', 'interrupted', 'unavailable', 'ready'].includes(desktop.value?.state)) {
26
+ ctx.effect(() => { const style = document.createElement('style'); style.dataset.plugin = 'arcfold-desktop-setup';
27
+ style.textContent = '.arcfold-desktop-setup{max-width:42rem;margin:1rem auto;padding:1.5rem;line-height:1.6}.arcfold-desktop-setup code{overflow-wrap:anywhere}.arcfold-desktop-setup button{margin-top:1rem;padding:.5rem .9rem}.arcfold-desktop-setup label{display:block}';
28
+ document.head.append(style); return () => style.remove(); }, 'Arcfold Desktop setup style');
29
+ ctx.slots.inject('settings.models.footer', () => ctx.slots.register({ name: 'settings.models.footer', id: 'arcfold-deepseek-credential',
30
+ order: 11, inject: () => ({ rpc: ctx.connection.rpc }) }, DesktopModels));
31
+ }
32
+ if (desktop?.ok === true && ['needs_setup', 'configured', 'activation_failed', 'interrupted', 'unavailable'].includes(desktop.value?.state)) {
33
+ const props = { initial: desktop.value, rpc: ctx.connection.rpc,
34
+ openWorkspace: path => openDesktopWorkspace(ctx.get('workspaces'), ctx.get('uiWorkspace'), path) };
35
+ for (const name of ['settings.section', 'conversation.hero.options'])
36
+ ctx.slots.inject(name, () => ctx.slots.register({ name, id: 'arcfold-desktop-setup', order: 11, label: 'Arcfold 设置', inject: () => props }, DesktopSetup));
37
+ return;
38
+ }
39
+ const mode = await ctx.connection.rpc.call('/api', 'arcfoldMode/current', { args: {} });
40
+ if (mode?.ok !== true || typeof mode.value?.goalTreeV3 !== 'boolean') throw new Error('Arcfold 页面模式尚未确认,请重新连接。');
41
+ if (mode.value.goalTreeV3) {
42
+ ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({ name: 'conversation.input.dock', id: 'goal', priority: -1 }, () => null));
43
+ return installTreeClient(ctx);
44
+ }
45
+ // Goals live in the Arcfold view. The official list-slot override removes
46
+ // only its composer control; ui-goal keeps rendering original goal history.
47
+ ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
48
+ name: 'conversation.input.dock', id: 'goal', priority: -1,
49
+ }, () => null));
50
+ const sources = new Set(), sessions = new Map();
51
+ const browseScope = createBrowseScopeSource(sources);
52
+ ctx.effect(() => ctx.locale.register(NS, { zh: { ...zh, ...goalZh, ...modelsZh, ...understandingZh, ...importsZh, ...opportunitiesZh }, en: { ...en, ...goalEn, ...modelsEn, ...understandingEn, ...importsEn, ...opportunitiesEn } }), 'Arcfold method UI copy');
53
+ ctx.effect(() => {
54
+ const style = document.createElement('style');
55
+ style.dataset.plugin = '@arcfold/dsh-method-ui';
56
+ style.textContent = typeof __ARCFOLD_METHOD_CSS__ === 'string' ? __ARCFOLD_METHOD_CSS__ : '';
57
+ document.head.append(style);
58
+ return () => style.remove();
59
+ }, 'Arcfold method UI style');
60
+ ctx.effect(() => () => { browseScope.dispose(); for (const source of sources) source.dispose(); sources.clear(); sessions.clear(); }, 'Arcfold method reads');
61
+ const modelSettings = createModelSettingsSource({ rpc: ctx.connection.rpc,
62
+ subscribeReset: listener => ctx.on('connection/reset', listener) });
63
+ sources.add(modelSettings);
64
+ const modelProps = {
65
+ hooks: { models: modelSettings },
66
+ onRefresh: () => modelSettings.refresh(),
67
+ onSaveLearning: request => modelSettings.saveLearning(request),
68
+ onStoreCredential: (provider, value) => modelSettings.storeCredential(provider, value),
69
+ onRemoveCredential: provider => modelSettings.removeCredential(provider),
70
+ };
71
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
72
+ name: 'settings.section', id: 'arcfold-models', order: 11, locale: NS,
73
+ label: () => ctx.locale.bind(NS)('modelsTitle'),
74
+ inject: () => modelProps,
75
+ }, ModelSettings));
76
+ // Hero and tab share the same session-bound sources. Slot changes only
77
+ // unsubscribe; the source remains reusable until the plugin is unloaded.
78
+ const sessionProps = sessionId => {
79
+ if (sessions.has(sessionId)) return sessions.get(sessionId);
80
+ const viewScope = browseScope.getSnapshot().viewScope;
81
+ const source = createMethodSource({ rpc: ctx.connection.rpc, sessionId, viewScope,
82
+ subscribeReset: listener => ctx.on('connection/reset', listener) });
83
+ sources.add(source);
84
+ const goals = createGoalSource({ rpc: ctx.connection.rpc, sessionId, viewScope,
85
+ subscribeReset: listener => ctx.on('connection/reset', listener) });
86
+ sources.add(goals);
87
+ const understandings = createUnderstandingSource({ rpc: ctx.connection.rpc, sessionId, viewScope,
88
+ subscribeReset: listener => ctx.on('connection/reset', listener) });
89
+ sources.add(understandings);
90
+ const imports = createImportSource({ rpc: ctx.connection.rpc, fileUpload: ctx.fileUpload, sessionId, viewScope,
91
+ subscribeReset: listener => ctx.on('connection/reset', listener) });
92
+ sources.add(imports);
93
+ const opportunities = createOpportunitySource({ rpc: ctx.connection.rpc, sessionId, viewScope, subscribeReset: listener => ctx.on('connection/reset', listener) });
94
+ sources.add(opportunities);
95
+ const props = { ...modelProps, onModelsRefresh: modelProps.onRefresh, opportunitySessionId: sessionId, understandingSessionId: sessionId, importSessionId: sessionId, hooks: { models: modelSettings, browseScope, methods: source, goals, understandings, imports, opportunities },
96
+ onViewScopeChange: viewScope => browseScope.setViewScope(viewScope),
97
+ onOpportunityDetails: id => opportunities.details(id),
98
+ onOpportunityAct: (endpoint, request) => opportunities.act(endpoint, request),
99
+ onOpportunityRefresh: () => opportunities.refresh(),
100
+ onOpportunityPage: offset => opportunities.refresh(offset),
101
+ onImportUpload: (file, onProgress) => imports.upload(file, onProgress),
102
+ onImportPrepare: request => imports.prepare(request),
103
+ onImportDetails: request => imports.details(request),
104
+ onImportConfirm: request => imports.confirm(request),
105
+ onImportWithdraw: request => imports.withdraw(request),
106
+ onImportRefresh: () => imports.refresh(),
107
+ onImportPage: offset => imports.refresh(offset),
108
+ onDetails: (methodId, offset) => source.details(methodId, offset),
109
+ onDecide: request => source.decide(request),
110
+ onAcknowledge: id => source.acknowledge(id),
111
+ onRefresh: () => source.refresh(),
112
+ onPage: offset => source.refresh(offset),
113
+ onGoalDetails: id => goals.details(id), onGoalDecide: request => goals.decide(request),
114
+ onGoalDiscuss: request => goals.discuss(request), onGoalRefresh: () => goals.refresh(),
115
+ onGoalStopWork: request => goals.stopWork(request),
116
+ onGoalResumeWork: request => goals.resumeWork(request),
117
+ onGoalPage: offset => goals.refresh(offset),
118
+ onUnderstandingDetails: id => understandings.details(id),
119
+ onUnderstandingDecide: request => understandings.decide(request),
120
+ onUnderstandingCorrect: request => understandings.correct(request),
121
+ onUnderstandingRefresh: () => understandings.refresh(),
122
+ onUnderstandingPage: offset => understandings.refresh(offset),
123
+ };
124
+ sessions.set(sessionId, props);
125
+ return props;
126
+ };
127
+ ctx.slots.inject('conversation.view', () => ctx.slots.register({
128
+ name: 'conversation.view', id: 'arcfold', order: 20, locale: NS,
129
+ label: () => ctx.locale.bind(NS)('collaborationTitle'),
130
+ inject: sessionProps,
131
+ }, CollaborationView));
132
+ // The host renders hero.options only for a real blank session. Its normal
133
+ // view tabs are hidden then, so browsing never requires a model message.
134
+ ctx.slots.inject('conversation.hero.options', () => ctx.slots.register({
135
+ name: 'conversation.hero.options', id: 'arcfold', order: 20, locale: NS,
136
+ inject: sessionProps,
137
+ }, CollaborationHero));
138
+ }
@@ -0,0 +1,30 @@
1
+ import { TypertRemoteService, RemoteError } from '@deepseek-ai/dsh-typert-protocol';
2
+ const service = 'arcfoldDesktopWeb', namespace = 'arcfoldDesktop';
3
+
4
+ export async function desktopRequest({ setup, connection }, method, request, signal, peer) {
5
+ if (!peer || peer !== connection.browserPeer || peer.principal?.kind !== 'owner' ||
6
+ peer.interactions !== 'answer' || peer.signal?.aborted !== false || signal?.aborted !== false)
7
+ throw new RemoteError('trusted_browser_required', '当前连接尚未认证。', {});
8
+ if (method === 'status') return setup?.status() ?? { state: 'not_desktop' };
9
+ if (method !== 'start' || !request || typeof request !== 'object' || Array.isArray(request) ||
10
+ Object.keys(request).join() !== 'confirmed' || request.confirmed !== true)
11
+ throw new RemoteError('invalid_desktop_setup', '请确认模型使用与费用后再继续。', {});
12
+ if (!setup?.start) throw new RemoteError('desktop_setup_unavailable', '当前环境无法进行 Desktop 设置。', {});
13
+ signal.throwIfAborted();
14
+ try { return await setup.start({ confirmed: true }); }
15
+ catch { throw new RemoteError('desktop_setup_failed', '设置未完成。请刷新并查看当前状态,避免重复创建数据。', {}); }
16
+ }
17
+ class DesktopWeb extends TypertRemoteService {
18
+ constructor(ctx) { super(ctx, service, { namespace }); }
19
+ dispatch(method, request) {
20
+ const { signal, peer } = this.ctx.invocation ?? {};
21
+ return desktopRequest({ setup: this.ctx.get('arcfoldDesktopSetup'), connection: this.ctx.connection }, method, request, signal, peer);
22
+ }
23
+ status(request) { return this.dispatch('status', request); }
24
+ start(request) { return this.dispatch('start', request); }
25
+ }
26
+ export const desktopInvocations = ['status', 'start'].map(method => ({
27
+ id: `@arcfold/dsh-method-ui#${namespace}/${method}`, service, namespace, method, invocation: { kind: 'direct' },
28
+ parameters: [{ name: 'request', wire: 'request', source: 'json', codec: { mode: 'src-json' } }], result: { mode: 'src-json' },
29
+ }));
30
+ export function installDesktopWeb(ctx) { return new DesktopWeb(ctx); }
@@ -0,0 +1,8 @@
1
+ /** Use the host's native Workspace registration and navigation services. */
2
+ export async function openDesktopWorkspace(workspaces, navigation, path) {
3
+ if (!workspaces || !navigation || typeof path !== 'string' || !path.startsWith('/'))
4
+ throw new Error('DSH 工作区服务尚未就绪。');
5
+ const workspace = await workspaces.create({ path });
6
+ if (!workspace?.workspaceId) throw new Error('无法确认新工作区。');
7
+ await navigation.openWorkspace(workspace.workspaceId, undefined, { agentPreset: 'arcfold-main' });
8
+ }
@@ -0,0 +1,133 @@
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
+
5
+ const NAMESPACE = 'arcfoldGoals', SERVICE = 'arcfoldGoalWeb';
6
+ const ENDPOINTS = ['list', 'details', 'decide', 'discuss', 'stopWork', 'resumeWork'];
7
+ const invalid = message => { throw Object.assign(new Error(message), { code: 'invalid_goal_request' }); };
8
+ function object(value, allowed, required = allowed) {
9
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
10
+ Object.keys(value).some(key => !allowed.includes(key)) || required.some(key => !Object.hasOwn(value, key)))
11
+ invalid('目标请求包含缺失或未允许的字段。');
12
+ }
13
+ function identity(value) {
14
+ if (typeof value !== 'string' || !value.trim() || value.length > 512 || value.includes('\0'))
15
+ invalid('请求身份不能为空或超长。');
16
+ }
17
+ function version(payload) {
18
+ if (!Number.isSafeInteger(payload.expectedRevision) || payload.expectedRevision < 1 ||
19
+ !Number.isSafeInteger(payload.definitionVersion) || payload.definitionVersion < 1)
20
+ invalid('操作必须绑定已查看的目标版本。');
21
+ }
22
+ function trusted(connection, signal, peer) {
23
+ if (!peer || peer !== connection.browserPeer || peer.principal?.kind !== 'owner' ||
24
+ peer.interactions !== 'answer' || peer.signal?.aborted !== false || signal?.aborted !== false)
25
+ throw Object.assign(new Error('此操作只接受当前已认证网页的请求。'), { code: 'trusted_browser_required' });
26
+ }
27
+
28
+ // The map coalesces only concurrent delivery of the same explicit submission.
29
+ // Durable binding ownership, including unknown admission, belongs to GoalService.
30
+ export function createGoalHandler({ goals, connection, agents, agentPresets, prompt, inflight = new Map() }) {
31
+ const assertMain = (sessionId, expected) => {
32
+ const agent = currentMainAgent({ agents, agentPresets }, sessionId);
33
+ if (!agent || expected && agent !== expected)
34
+ throw Object.assign(new Error('请打开当前的 Arcfold 主交流,再从这里发送目标讨论。'), { code: 'goal_main_session_required' });
35
+ return agent;
36
+ };
37
+ return async (endpoint, payload, signal, peer) => {
38
+ try {
39
+ trusted(connection, signal, peer);
40
+ let value;
41
+ if (endpoint === 'list') {
42
+ object(payload, ['sessionId', 'offset', 'limit', 'viewScope'], ['sessionId']); identity(payload.sessionId); browseScope(payload.viewScope);
43
+ if (payload.offset !== undefined && (!Number.isSafeInteger(payload.offset) || payload.offset < 0) ||
44
+ payload.limit !== undefined && (!Number.isSafeInteger(payload.limit) || payload.limit < 1 || payload.limit > 100))
45
+ invalid('目标分页参数不合法。');
46
+ value = { ...goals.list(payload), canDiscuss: currentMainAgent({ agents, agentPresets }, payload.sessionId) !== null };
47
+ } else if (endpoint === 'details') {
48
+ object(payload, ['sessionId', 'goalId', 'viewScope'], ['sessionId', 'goalId']); identity(payload.sessionId); identity(payload.goalId); browseScope(payload.viewScope);
49
+ value = goals.details(payload.goalId, { sessionId: payload.sessionId,
50
+ ...(payload.viewScope !== undefined ? { viewScope: payload.viewScope } : {}) });
51
+ } else if (endpoint === 'stopWork' || endpoint === 'resumeWork') {
52
+ object(payload, ['sessionId', 'workId', 'nativeGoalId', 'nativeRevision', 'requestId']);
53
+ for (const key of ['sessionId', 'workId', 'nativeGoalId', 'requestId']) identity(payload[key]);
54
+ if (!Number.isSafeInteger(payload.nativeRevision) || payload.nativeRevision < 1) invalid('执行版本不合法。');
55
+ const agent = assertMain(payload.sessionId);
56
+ value = goals.controlWork({ ...payload, agent, action: endpoint === 'stopWork' ? 'pause' : 'resume',
57
+ userIdentity: 'browser:' + peer.id });
58
+ } else if (endpoint === 'decide') {
59
+ object(payload, ['goalId', 'expectedRevision', 'definitionVersion', 'action', 'requestId', 'sessionId'],
60
+ ['goalId', 'expectedRevision', 'definitionVersion', 'action', 'requestId']);
61
+ if (payload.sessionId) assertMain(payload.sessionId);
62
+ identity(payload.goalId); identity(payload.requestId); version(payload);
63
+ if (!['pause', 'resume', 'close'].includes(payload.action)) invalid('目标决定必须是暂停、恢复或结束追踪。');
64
+ value = goals.decide({ ...payload, userIdentity: 'browser:' + peer.id });
65
+ } else if (endpoint === 'discuss') {
66
+ object(payload, ['sessionId', 'goalId', 'expectedRevision', 'definitionVersion', 'mode', 'text', 'requestId']);
67
+ identity(payload.sessionId); identity(payload.goalId); identity(payload.requestId); version(payload);
68
+ if (!['discuss', 'repair'].includes(payload.mode) || typeof payload.text !== 'string' ||
69
+ !payload.text.trim() || payload.text.length > 24000 || payload.text.includes('\0'))
70
+ invalid('请填写本次要发送的原话,长度不超过 24000 字。');
71
+ const agent = assertMain(payload.sessionId);
72
+ const content = [{ type: 'text', text: payload.text }];
73
+ const { text: _text, ...request } = payload;
74
+ const bound = goals.bindDiscussion({ ...request, content, userIdentity: 'browser:' + peer.id });
75
+ const key = bound.binding.id;
76
+ // A repeated request is validated by business before joining the in-flight call.
77
+ if (inflight.has(key)) value = inflight.get(key);
78
+ else if (!bound.created) value = bound; // No retry of failed, unknown or crash-interrupted admission.
79
+ else {
80
+ let enteredPrompt = false;
81
+ const pending = Promise.resolve().then(async () => {
82
+ let state;
83
+ try {
84
+ trusted(connection, signal, peer);
85
+ assertMain(payload.sessionId, agent);
86
+ enteredPrompt = true;
87
+ const result = await prompt({ sessionId: payload.sessionId, requestId: payload.requestId,
88
+ mode: 'queue', content }, signal);
89
+ state = result?.accepted === true ? 'accepted' : 'unknown';
90
+ } catch { state = enteredPrompt ? 'unknown' : 'failed'; }
91
+ const binding = goals.recordDiscussionAdmission({ bindingId: key, state });
92
+ return { ...bound, binding };
93
+ });
94
+ inflight.set(key, pending);
95
+ // Do not start another call if the caller disconnects or recording fails.
96
+ value = pending.finally(() => { if (inflight.get(key) === pending) inflight.delete(key); });
97
+ }
98
+ } else throw Object.assign(new Error('不存在的目标操作。'), { code: 'goal_endpoint_not_found' });
99
+ return { ok: true, value: await value };
100
+ } catch (error) {
101
+ return { ok: false, error: { code: typeof error?.code === 'string' ? error.code : 'goal_request_failed',
102
+ message: error instanceof Error ? error.message : '目标操作未完成。', details: {} } };
103
+ }
104
+ };
105
+ }
106
+
107
+ class GoalWeb extends TypertRemoteService {
108
+ inflight = new Map();
109
+ constructor(ctx) { super(ctx, SERVICE, { namespace: NAMESPACE }); }
110
+ async dispatch(endpoint, request) {
111
+ const invocation = this.ctx.invocation;
112
+ // Access through this call-derived Context preserves the original Gateway invocation.
113
+ const controller = this.ctx.sessionController;
114
+ const result = await createGoalHandler({ goals: this.ctx.arcfoldGoals, connection: this.ctx.connection, agents: this.ctx.agents, agentPresets: this.ctx.agentPresets,
115
+ inflight: this.inflight, prompt: (payload, signal) => controller.prompt(payload, signal) })(
116
+ endpoint, request, invocation?.signal, invocation?.peer);
117
+ if (!result.ok) throw new RemoteError(result.error.code, result.error.message, result.error.details);
118
+ return result.value;
119
+ }
120
+ list(request) { return this.dispatch('list', request); }
121
+ details(request) { return this.dispatch('details', request); }
122
+ decide(request) { return this.dispatch('decide', request); }
123
+ discuss(request) { return this.dispatch('discuss', request); }
124
+ stopWork(request) { return this.dispatch('stopWork', request); }
125
+ resumeWork(request) { return this.dispatch('resumeWork', request); }
126
+ }
127
+
128
+ export const goalInvocations = ENDPOINTS.map(method => ({ id: `@arcfold/dsh-method-ui#${NAMESPACE}/${method}`,
129
+ service: SERVICE, namespace: NAMESPACE, method, invocation: { kind: 'direct' },
130
+ parameters: [{ name: 'request', wire: 'request', source: 'json', codec: { mode: 'src-json' } }], result: { mode: 'src-json' } }));
131
+ export function installGoalWeb(ctx) {
132
+ return ctx.inject(['arcfoldGoals', 'sessionController', 'agents', 'agentPresets', 'connection'], function goalWeb(owner) { new GoalWeb(owner); });
133
+ }
@@ -0,0 +1,78 @@
1
+ export const goalZh = {
2
+ goalCurrentWork: '当前工作', goalNoCurrentWork: '此目标尚未关联具体工作。', goalOtherWork: '其他工作',
3
+ goalEvidence: '目标依据与历史记录', goalUnavailable: '目标内容暂不可用', goalWorkOriginal: '完整委托与来源',
4
+ goalBlockReason: '阻塞原因', goalBlockUnknown: '执行已受阻,但尚未记录具体原因。',
5
+ goalBlockRoundLimit: '已达到这次委托允许的连续执行次数。',
6
+ goalBlockQueueFailed: '下一轮工作未能加入执行队列。', goalBlockPromptRejected: '下一轮工作在开始前被拒绝。',
7
+ goalDraftChanged: '目标内容已更新。草稿已保留,请核对当前内容并编辑草稿后再发送。',
8
+ goalTitle: '长期目标', goalOpen: '查看目标', goalHide: '收起目标', goalEmpty: '目前没有可显示的长期目标。',
9
+ goalLoading: '正在读取目标…', goalOutcome: '希望实现的结果', goalProgress: '已记录的进展', goalRelations: '与其他目标的关系',
10
+ goalDefinition: '定义版本', goalTracking: '追踪中', goalPaused: '已暂停追踪', goalClosed: '已结束追踪',
11
+ goalPart: '组成', goalSupports: '支持', goalDerived: '由此形成', goalRelationReview: '定义已变化,关系待复核',
12
+ goalRelationUnavailable: '关系依据已失效', goalResume: '恢复追踪', goalPause: '暂停追踪', goalClose: '结束追踪',
13
+ goalBoundary: 'Arcfold 从交流和共同经历中整理目标,把相关工作、背景和进展联系起来。你可以在对话中补充或纠正;一次工作完成不代表长期目标已经实现。',
14
+ goalSaved: '追踪决定已保存,不会因此启动工作。', goalDiscuss: '围绕此目标发送', goalRepair: '用新原话重新澄清',
15
+ goalDiscussLabel: '本次要发送的原话', goalDiscussHelp: '点击后把这段原话发送到当前交流,并只为这一次消息关联所选目标;不会恢复暂停或结束的追踪。',
16
+ goalRepairHelp: '旧依据已经失效。这里只发送你这次的新原话供重新澄清,不引用失效旧内容,也不恢复追踪。',
17
+ goalAccepted: '宿主已受理这次发送;目标关联须等这条真实消息进入当前工作后才生效。',
18
+ goalUnknown: '这次发送是否受理尚未确认。再次点击只查询同一请求,不会自动重发。请先核对当前交流。',
19
+ goalFailed: '发送前已停止;没有重新发送。请核对当前交流后另行决定。',
20
+ goalConflict: '目标已变化,请读完更新后的目标,再明确决定是否发送或改变追踪状态。',
21
+ goalLinkedWork: '已关联的工作', goalContribution: '助手当时的关联判断(依据当时定义版本)', goalUnassessed: '贡献效果尚未评估',
22
+ goalContributionUnavailable: '关联判断的依据已失效或无法核对,暂不展示旧正文。',
23
+ goalProgressUnavailable: '进展依据已失效,暂不作为当前依据。',
24
+ goalNoProgress: '尚未记录进展。', goalNoRelations: '尚未记录目标关系。', goalNoLinks: '尚未关联工作。',
25
+ goalHistory: '本交流最近的目标讨论发送记录', goalOriginal: '发送原话', goalAdmission: '受理状态',
26
+ goalMainRequired: '请打开当前的 Arcfold 主交流,再从这里发送目标讨论。',
27
+ goalWorkTitle: '相关工作与实际进展', goalWorkHelp: '要修改或继续工作,直接在对话中告诉 Arcfold。停止推进只暂停这件工作的自动继续。',
28
+ goalWorkEnabled: '持续推进已开启', goalWorkDisarmed: '待继续', goalWorkPaused: '已暂停',
29
+ goalWorkBlocked: '推进受阻', goalWorkComplete: '这件工作已完成', goalWorkRounds: '已开始的工作轮次',
30
+ goalWorkVerified: '已核对原始委托', goalWorkLegacy: '历史委托尚未核对', goalWorkFor: '对应目标:',
31
+ goalWorkUnlinked: '目标联系待整理', goalWorkRelationReview: '这项联系需要重新核对。',
32
+ goalWorkReadGoal: '查看对应目标', goalWorkStop: '停止这件工作的推进', goalWorkStopped: '这件工作的后续推进已暂停。',
33
+ goalWorkUnknown: '操作结果尚未确认,请先刷新查看实际状态。再次点击会核对同一请求。',
34
+ goalWorkResume: '继续这件工作', goalWorkResumed: '这件工作已启用后续推进。', goalWorkDetails: '查看工作详情',
35
+ goalWorkLinked: '已关联目标', goalCorrection: '补充或纠正',
36
+ goalWorkCleared: '不再作为当前工作', goalWorkSourceUnavailable: '委托来源已失效',
37
+ goalStageWaiting: '等待明确下一步', goalStageComplete: '当前阶段已完成', goalStageReview: '待明确当前阶段',
38
+ goalStageDoneWhen: '完成条件:', goalStageBoundaries: '推进范围:',
39
+ };
40
+ export const goalEn = {
41
+ goalCurrentWork: 'Current work', goalNoCurrentWork: 'No work is linked to this goal yet.', goalOtherWork: 'Other work',
42
+ goalEvidence: 'Goal sources and history', goalUnavailable: 'Goal content unavailable', goalWorkOriginal: 'Full delegation and sources',
43
+ goalBlockReason: 'Blocking reason', goalBlockUnknown: 'Execution is blocked; no specific reason is recorded.',
44
+ goalBlockRoundLimit: 'This delegation reached its allowed number of continuation rounds.',
45
+ goalBlockQueueFailed: 'The next round could not be queued.', goalBlockPromptRejected: 'The next round was rejected before starting.',
46
+ goalDraftChanged: 'The goal changed. Your draft is preserved; review the current content and edit your draft before sending.',
47
+ goalTitle: 'Long-term goals', goalOpen: 'Read goal', goalHide: 'Hide goals', goalEmpty: 'No long-term goals are available.',
48
+ goalLoading: 'Loading goals…', goalOutcome: 'Intended outcome', goalProgress: 'Recorded progress', goalRelations: 'Related goals',
49
+ goalDefinition: 'Definition version', goalTracking: 'Tracking', goalPaused: 'Tracking paused', goalClosed: 'Tracking ended',
50
+ goalPart: 'Part of', goalSupports: 'Supports', goalDerived: 'Derived from', goalRelationReview: 'Definition changed; relation needs review',
51
+ goalRelationUnavailable: 'Relation evidence unavailable', goalResume: 'Resume tracking', goalPause: 'Pause tracking', goalClose: 'End tracking',
52
+ goalBoundary: 'Arcfold connects goals, related work, context and progress from conversations and shared experience. Clarify or correct them in conversation. Completing a piece of work does not establish that a long-term goal has been achieved.',
53
+ goalSaved: 'Tracking decision saved. No work is started by this decision.', goalDiscuss: 'Send about this goal', goalRepair: 'Clarify with new words',
54
+ goalDiscussLabel: 'Your words to send', goalDiscussHelp: 'Send these exact words to this conversation and relate only that submission to this goal. Paused or ended tracking remains unchanged.',
55
+ goalRepairHelp: 'Prior evidence is unavailable. Only your new words will be sent for clarification; invalid old content is not used and tracking is not resumed.',
56
+ goalAccepted: 'The host accepted the submission. The relation takes effect only when that real message enters the current work.',
57
+ goalUnknown: 'Admission is unconfirmed. Clicking again only reads the same request and never resends it. Check the conversation first.',
58
+ goalFailed: 'Stopped before submission; not resent. Check the conversation before deciding what to do next.',
59
+ goalConflict: 'The goal changed. Read the updated goal before deciding again.',
60
+ goalLinkedWork: 'Related work', goalContribution: 'Assistant interpretation at the recorded definition version', goalUnassessed: 'Contribution effect not assessed',
61
+ goalContributionUnavailable: 'The interpretation evidence is unavailable or cannot be verified; prior wording is not shown.',
62
+ goalProgressUnavailable: 'Progress evidence is unavailable and is not used as current evidence.',
63
+ goalNoProgress: 'No progress is recorded.', goalNoRelations: 'No relations are recorded.', goalNoLinks: 'No work is linked.',
64
+ goalHistory: 'Recent goal discussion submissions in this conversation', goalOriginal: 'Submitted words', goalAdmission: 'Admission',
65
+ goalMainRequired: 'Open the current Arcfold main conversation to send a goal discussion here.',
66
+ goalWorkTitle: 'Related work and actual progress', goalWorkHelp: 'Tell Arcfold in conversation when you want to change or continue work. Stopping pauses only this work’s automatic continuation.',
67
+ goalWorkEnabled: 'Automatic continuation enabled', goalWorkDisarmed: 'Awaiting continuation', goalWorkPaused: 'Paused',
68
+ goalWorkBlocked: 'Work blocked', goalWorkComplete: 'This work is complete', goalWorkRounds: 'Rounds started',
69
+ goalWorkVerified: 'Original delegation verified', goalWorkLegacy: 'Historical delegation not yet verified', goalWorkFor: 'Related goal:',
70
+ goalWorkUnlinked: 'Goal relation not yet recorded', goalWorkRelationReview: 'This relation needs to be checked again.',
71
+ goalWorkReadGoal: 'Read related goal', goalWorkStop: 'Stop this work’s continuation', goalWorkStopped: 'This work’s continuation is paused.',
72
+ goalWorkUnknown: 'The action result is unconfirmed. Refresh to inspect actual execution. Clicking again checks the same request.',
73
+ goalWorkResume: 'Continue this work', goalWorkResumed: 'Continuation is enabled for this work.', goalWorkDetails: 'Read work details',
74
+ goalWorkLinked: 'A related goal', goalCorrection: 'Clarify or correct',
75
+ goalWorkCleared: 'No longer the current work', goalWorkSourceUnavailable: 'Delegation source is unavailable',
76
+ goalStageWaiting: 'Awaiting the next decision', goalStageComplete: 'Current stage completed', goalStageReview: 'Current stage needs clarification',
77
+ goalStageDoneWhen: 'Completion criteria:', goalStageBoundaries: 'Scope:',
78
+ };
@@ -0,0 +1,72 @@
1
+ import { requireViewScope, viewScopeFields } from './browse-source.mjs';
2
+ export async function callGoal(rpc, endpoint, payload, signal) {
3
+ const result = await rpc.call('/api', `arcfoldGoals/${endpoint}`, { args: { request: payload } }, signal);
4
+ if (result?.ok !== true) throw Object.assign(new Error(result?.error?.message || '目标请求未完成。'),
5
+ { code: result?.error?.code || 'goal_request_failed', remote: true });
6
+ return result.value;
7
+ }
8
+
9
+ // Read-only UI cache. Mutations occur only in explicit click handlers.
10
+ export function createGoalSource({ rpc, sessionId, viewScope, subscribeReset, intervalMs = 5000 }) {
11
+ viewScopeFields(viewScope);
12
+ let snapshot = { loading: true, error: null, items: [], works: [], total: 0, offset: 0, nextOffset: null, readEpoch: 0 };
13
+ let generation = 0, controller, timer, reset, disposed = false;
14
+ const listeners = new Set(), reads = new Set();
15
+ const publish = value => { snapshot = { ...snapshot, ...value }; for (const listener of listeners) listener(); };
16
+ const cancelled = () => Object.assign(new Error('查看范围已变化,请重新打开目标。'), { code: 'goal_read_cancelled' });
17
+ function invalidateReads({ clear = true } = {}) {
18
+ controller?.abort(); generation++;
19
+ for (const read of reads) read.abort();
20
+ publish({ loading: true, error: null, readEpoch: snapshot.readEpoch + 1,
21
+ ...(clear ? { items: [], works: [], total: 0, offset: 0, nextOffset: null, canDiscuss: false } : {}) });
22
+ }
23
+ async function refresh(offset = snapshot.offset) {
24
+ if (disposed) return;
25
+ controller?.abort(); controller = new AbortController();
26
+ const current = ++generation, signal = controller.signal;
27
+ try {
28
+ const result = await callGoal(rpc, 'list', { sessionId, offset, limit: 20, ...viewScopeFields(viewScope) }, signal);
29
+ if (current !== generation || signal.aborted || disposed) return;
30
+ publish({ ...result, offset, loading: false, error: null });
31
+ } catch (error) {
32
+ if (current === generation && !signal.aborted && !disposed)
33
+ publish({ loading: false, error: error instanceof Error ? error.message : '读取目标失败。' });
34
+ }
35
+ }
36
+ function stop() { clearInterval(timer); timer = undefined; reset?.(); reset = undefined; invalidateReads(); }
37
+ return {
38
+ getSnapshot: () => snapshot,
39
+ subscribe(listener) {
40
+ if (disposed) return () => {};
41
+ listeners.add(listener);
42
+ if (listeners.size === 1) {
43
+ void refresh(); timer = setInterval(() => { void refresh(); }, intervalMs);
44
+ // A reconnect invalidates pending reads, but does not erase the user's
45
+ // unsent goal discussion. A changed goal revision still makes it stale.
46
+ reset = subscribeReset(() => { invalidateReads({ clear: false }); void refresh(); });
47
+ }
48
+ return () => { listeners.delete(listener); if (!listeners.size) stop(); };
49
+ },
50
+ refresh,
51
+ setViewScope(value) {
52
+ requireViewScope(value);
53
+ if (disposed || viewScope === value) return;
54
+ viewScope = value; invalidateReads();
55
+ if (listeners.size) void refresh(0);
56
+ },
57
+ async details(goalId) {
58
+ if (disposed) throw cancelled();
59
+ const read = new AbortController(), epoch = snapshot.readEpoch; reads.add(read);
60
+ try {
61
+ const value = await callGoal(rpc, 'details', { goalId, sessionId, ...viewScopeFields(viewScope) }, read.signal);
62
+ if (disposed || read.signal.aborted || epoch !== snapshot.readEpoch) throw cancelled();
63
+ return value;
64
+ } finally { reads.delete(read); }
65
+ },
66
+ async decide(request) { const result = await callGoal(rpc, 'decide', request); await refresh(); return result; },
67
+ async stopWork(request) { const result = await callGoal(rpc, 'stopWork', { ...request, sessionId }); await refresh(); return result; },
68
+ async resumeWork(request) { const result = await callGoal(rpc, 'resumeWork', { ...request, sessionId }); await refresh(); return result; },
69
+ discuss: request => callGoal(rpc, 'discuss', { ...request, sessionId }),
70
+ dispose() { disposed = true; stop(); listeners.clear(); },
71
+ };
72
+ }
@@ -0,0 +1,98 @@
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='arcfoldImports',SERVICE='arcfoldImportWeb';
5
+ const ENDPOINTS=['prepare','list','details','confirm','withdraw'];
6
+ export const MAX_IMPORT_FILE_BYTES=805306368;
7
+ const reject=(code,message)=>{throw new RemoteError(code,message,{});};
8
+ function shape(value,allowed,required=allowed){
9
+ if(!value||typeof value!=='object'||Array.isArray(value)||Object.keys(value).some(k=>!allowed.includes(k))||required.some(k=>!Object.hasOwn(value,k)))
10
+ reject('invalid_import_request','导入请求含缺失或未允许的字段。');
11
+ }
12
+ function id(value){if(typeof value!=='string'||!value.trim()||value.length>512||value.includes('\0'))reject('invalid_import_request','请求身份无效。');}
13
+ function trusted(connection,signal,peer){
14
+ if(!peer||peer!==connection.browserPeer||peer.principal?.kind!=='owner'||peer.interactions!=='answer'||peer.signal?.aborted!==false||signal?.aborted!==false)
15
+ reject('trusted_browser_required','导入只接受当前已认证网页所有者的请求。');
16
+ }
17
+ const errors={revision_conflict:'预览版本已变化,请重新查看。',import_identity_conflict:'提交编号已绑定另一份内容,未重发。',
18
+ invalid_browse_scope:'请选择全部积累或与当前聊天相关的内容。',
19
+ invalid_import_page:'分页范围无效。',import_not_found:'导入批次不存在。',import_text_denied:'原文不属于此批预览。',
20
+ import_not_confirmable:'请在准备预览的同一个主交流确认完整预览。',import_not_withdrawable:'此批次尚不可撤销。',
21
+ import_already_withdrawn:'相同来源批次已撤销,重复准备不会重新启用。',import_dependency_changed:'已保存预览的证据已变化,未导入。',
22
+ invalid_import_scope:'只能选择一个旧交流映射到当前主交流。',invalid_import_request:'导入请求无效。',invalid_import_file:'附件大小或摘要无效。'};
23
+
24
+ export function createImportHandler({imports,connection,agents,agentPresets,fileUploads,attachments}){
25
+ const main=sessionId=>{
26
+ const agent=currentMainAgent({agents,agentPresets},sessionId);
27
+ if(!agent)
28
+ reject('import_session_denied','请在当前已打开的 Arcfold 主交流导入。');
29
+ return agent;
30
+ };
31
+ return async(endpoint,payload,signal,peer)=>{
32
+ try{
33
+ trusted(connection,signal,peer);
34
+ if(!imports)reject('import_unavailable','当前部署尚未接通导入。');
35
+ const fields=endpoint==='prepare'?['sessionId','receiptId','requestId','scopeMap']:
36
+ endpoint==='list'?['sessionId','offset','limit','viewScope']:endpoint==='details'?['sessionId','batchId','view','offset','limit','textKey','viewScope']:
37
+ ['sessionId','batchId','expectedRevision','previewHash','requestId'];
38
+ const required=endpoint==='prepare'?['sessionId','receiptId','requestId']:endpoint==='list'?['sessionId']:
39
+ endpoint==='details'?['sessionId','batchId']:fields;
40
+ if(!ENDPOINTS.includes(endpoint))reject('import_endpoint_not_found','不存在的导入操作。');
41
+ shape(payload,fields,required);id(payload.sessionId);
42
+ const reading=endpoint==='list'||endpoint==='details';
43
+ if(reading)browseScope(payload.viewScope,'all');
44
+ if(reading&&payload.viewScope!==undefined)return {...imports[endpoint](payload),
45
+ canImport:currentMainAgent({agents,agentPresets},payload.sessionId)!==null};
46
+ const agent=main(payload.sessionId);
47
+ if(endpoint==='list'||endpoint==='details')return imports[endpoint](payload);
48
+ id(payload.requestId);
49
+ if(endpoint!=='prepare')return imports[endpoint]({...payload,userIdentity:'browser:'+peer.id});
50
+ id(payload.receiptId);
51
+ if(payload.scopeMap!==undefined){
52
+ if(!Array.isArray(payload.scopeMap)||payload.scopeMap.length>1)reject('invalid_import_scope',errors.invalid_import_scope);
53
+ for(const row of payload.scopeMap){shape(row,['sourceConversationId','targetSessionId']);id(row.sourceConversationId);
54
+ if(row.targetSessionId!==payload.sessionId)reject('invalid_import_scope',errors.invalid_import_scope);}
55
+ }
56
+ const prior=imports.replayPreparation(payload);if(prior)return await prior;
57
+ const staged=fileUploads?.resolve(agent,payload.receiptId),ref=staged?.attachment;
58
+ if(staged?.type!=='file'||!ref||typeof ref.attachmentId!=='string'||!/^sha256:[a-f0-9]{64}$/.test(ref.attachmentId)||
59
+ typeof ref.name!=='string'||ref.name.length>512||!Number.isSafeInteger(ref.bytes)||ref.bytes<0||ref.bytes>MAX_IMPORT_FILE_BYTES)
60
+ reject('import_receipt_denied','附件凭证不属于当前真实主交流,或超出导入上限。');
61
+ const frozen={attachmentId:ref.attachmentId,name:ref.name,bytes:ref.bytes};
62
+ const current=()=>{
63
+ trusted(connection,signal,peer);
64
+ if(main(payload.sessionId)!==agent||fileUploads.resolve(agent,payload.receiptId)!==staged||
65
+ ref.attachmentId!==frozen.attachmentId||ref.name!==frozen.name||ref.bytes!==frozen.bytes)
66
+ reject('import_receipt_changed','上传凭证或主交流已变化。');
67
+ };
68
+ current();
69
+ return await imports.prepare({...payload,file:{name:frozen.name,bytes:frozen.bytes,sha256:frozen.attachmentId.slice(7)},
70
+ userIdentity:'browser:'+peer.id,signal:AbortSignal.any([signal,peer.signal]),
71
+ read:async function*(abort){
72
+ current();for await(const chunk of attachments.readFileStream(frozen,abort)){current();abort.throwIfAborted();yield chunk;}
73
+ // Native digest check happens after its last yield; do not mark ready earlier.
74
+ current();abort.throwIfAborted();
75
+ }});
76
+ }catch(error){
77
+ if(error instanceof RemoteError)throw error;
78
+ const code=Object.hasOwn(errors,error?.code)?error.code:'import_request_failed';
79
+ throw new RemoteError(code,errors[code]||'操作未确认,请刷新批次记录。没有自动重试。',{});
80
+ }
81
+ };
82
+ }
83
+ class ImportWeb extends TypertRemoteService{
84
+ constructor(ctx){super(ctx,SERVICE,{namespace:NAMESPACE});}
85
+ dispatch(method,request){const {signal,peer}=this.ctx.invocation??{};
86
+ return createImportHandler({imports:this.ctx.arcfoldImports,connection:this.ctx.connection,agents:this.ctx.agents,
87
+ agentPresets:this.ctx.agentPresets,fileUploads:this.ctx.fileUploads,attachments:this.ctx.attachments})(method,request,signal,peer);}
88
+ prepare(value){return this.dispatch('prepare',value);}
89
+ list(value){return this.dispatch('list',value);}
90
+ details(value){return this.dispatch('details',value);}
91
+ confirm(value){return this.dispatch('confirm',value);}
92
+ withdraw(value){return this.dispatch('withdraw',value);}
93
+ }
94
+ export const importInvocations=ENDPOINTS.map(method=>({id:`@arcfold/dsh-method-ui#${NAMESPACE}/${method}`,
95
+ service:SERVICE,namespace:NAMESPACE,method,invocation:{kind:'direct'},
96
+ parameters:[{name:'request',wire:'request',source:'json',codec:{mode:'src-json'}}],result:{mode:'src-json'}}));
97
+ export function installImportWeb(ctx){return ctx.inject(['arcfoldImports','connection','agents','agentPresets','fileUploads','attachments'],
98
+ owner=>{new ImportWeb(owner);});}