dsh-mobile-plugin 0.0.0-stage → 0.1.57
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.
- package/README.md +128 -2
- package/config/agent-presets/standard/agent.cordis.yml +170 -0
- package/config/agent-presets/standard/preset.yml +3 -0
- package/cordis.mobile-host.yml +48 -0
- package/cordis.patch.yml +21 -0
- package/dsh-mobile-credentials/lib/credentials.js +135 -0
- package/dsh-mobile-credentials/package.json +13 -0
- package/host.js +128 -0
- package/lib/auto-archive.cjs +98 -0
- package/lib/client.js +690 -0
- package/lib/desktop-half-stats.cjs +101 -0
- package/lib/desktop-view.cjs +109 -0
- package/lib/host-guard.cjs +230 -0
- package/lib/host-patches.cjs +46 -0
- package/lib/http/package.json +3 -0
- package/lib/http/routing.js +84 -0
- package/lib/impl.js +406 -0
- package/lib/mirror/pusher.cjs +706 -0
- package/lib/mirror-snapshot.cjs +39 -0
- package/lib/mobile/agent-presets.js +182 -0
- package/lib/mobile/attachments.js +116 -0
- package/lib/mobile/auth.js +78 -0
- package/lib/mobile/completion.js +114 -0
- package/lib/mobile/credentials.js +150 -0
- package/lib/mobile/directory-browse.js +295 -0
- package/lib/mobile/envelope.js +51 -0
- package/lib/mobile/events.js +293 -0
- package/lib/mobile/history-derive.js +608 -0
- package/lib/mobile/host-bridge/endpoints.js +189 -0
- package/lib/mobile/host-bridge/event-synthesis.js +232 -0
- package/lib/mobile/host-bridge/shims.js +218 -0
- package/lib/mobile/host-bridge/transport.js +168 -0
- package/lib/mobile/host-bridge.js +71 -0
- package/lib/mobile/host-settings.js +282 -0
- package/lib/mobile/idempotency.js +78 -0
- package/lib/mobile/jobs.js +97 -0
- package/lib/mobile/local-images.js +153 -0
- package/lib/mobile/package.json +3 -0
- package/lib/mobile/plugins.js +149 -0
- package/lib/mobile/questions.js +71 -0
- package/lib/mobile/references.js +209 -0
- package/lib/mobile/rpc.js +76 -0
- package/lib/mobile/session-cache.js +263 -0
- package/lib/mobile/session-goal.js +44 -0
- package/lib/mobile/session-log.js +94 -0
- package/lib/mobile/session-mirror.js +177 -0
- package/lib/mobile/session-models.js +153 -0
- package/lib/mobile/session-presets.js +52 -0
- package/lib/mobile/session-search.js +65 -0
- package/lib/mobile/session-send.js +278 -0
- package/lib/mobile/session-snapshot.js +142 -0
- package/lib/mobile/sessions.js +462 -0
- package/lib/mobile/skills.js +80 -0
- package/lib/mobile/stream.js +268 -0
- package/lib/mobile/subagents.js +265 -0
- package/lib/mobile/workspaces.js +214 -0
- package/lib/panel/index.html +33 -0
- package/lib/read-receipt.cjs +124 -0
- package/lib/reconnect-scheduler.cjs +37 -0
- package/lib/relay/core.js +77 -0
- package/lib/relay/lan-network.cjs +85 -0
- package/lib/relay/lan-proxy.js +593 -0
- package/lib/relay/package.json +3 -0
- package/lib/relay/pairing-policy.js +45 -0
- package/lib/relay/port-allocator.cjs +60 -0
- package/lib/relay/preview.js +374 -0
- package/lib/relay/protocol.js +177 -0
- package/lib/relay/status.js +145 -0
- package/lib/relay/tunnel.js +721 -0
- package/lib/routes/control-plane.js +311 -0
- package/lib/routes/mobile-api.js +208 -0
- package/lib/routes/package.json +3 -0
- package/lib/session-cleanup.cjs +153 -0
- package/lib/shared/credentials-entry.js +48 -0
- package/lib/shared/dshtoken.js +62 -0
- package/lib/shared/fresh-request.js +35 -0
- package/lib/shared/http-util.js +71 -0
- package/lib/shared/list-revision.js +51 -0
- package/lib/shared/list-signal.js +87 -0
- package/lib/shared/package.json +3 -0
- package/lib/shared/paths.js +27 -0
- package/lib/shared/session-signal.js +110 -0
- package/lib/shared/workspace-guard.js +127 -0
- package/lib/sidecar-facade-worker.cjs +353 -0
- package/lib/sidecar-facade.cjs +141 -0
- package/lib/sidecar-lifecycle.cjs +246 -0
- package/package.json +74 -4
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* dsh-mobile 工作区/目录浏览:透传宿主 apiProxy 的目录浏览与工作区采纳 RPC。
|
|
5
|
+
*
|
|
6
|
+
* 移动端是**远程遥控器**——它不能像桌面端那样弹被控端的原生目录对话框。
|
|
7
|
+
*
|
|
8
|
+
* 这里曾经写着一个**错的**前提:以为 directory-picker-auto 会在「宿主绑定局域网(非
|
|
9
|
+
* 127.0.0.1)」时自动启用 `browse` 能力,于是 `apiProxy.host.listDirectory` /
|
|
10
|
+
* `host.createDirectory` 在任何被控端都可用。实际不是:auto 只在「web 服务绑到非回环」
|
|
11
|
+
* 或「平台不是 darwin/win32」时挂 browse,而**桌面 profile 宿主**(手机经 sidecar/中转
|
|
12
|
+
* 连上的那种)两者都不满足,于是恒解析成 native —— 手机端一个条目都列不出来,只能登记
|
|
13
|
+
* 一个恰好已存在的目录。现在这两个动词在宿主不服务 browse 时由插件自带实现兜底,见
|
|
14
|
+
* lib/mobile/directory-browse.js。本模块仍只做与 sessions.js 相同的透传包装。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { sendJson, readJsonBody } = require('../shared/http-util');
|
|
18
|
+
const { listRevision } = require('../shared/list-revision.js');
|
|
19
|
+
// 会话列表缓存归 sessions.js 管。归档/移动会话会改变列表行本身,必须让那边立刻失效,
|
|
20
|
+
// 否则手机端按完「归档会话」,那一行最长 10 秒还留在侧栏里,看着像没归档。
|
|
21
|
+
const { invalidateSessionList } = require('./sessions.js');
|
|
22
|
+
const { withIdempotency, requestIdOf } = require('./idempotency.js');
|
|
23
|
+
const { canonicalDeviceKey } = require('./session-cache.js');
|
|
24
|
+
const { unwrap } = require('./envelope.js');
|
|
25
|
+
|
|
26
|
+
/** 调宿主 apiProxy.host.*(directory-picker 的 browse 能力)。 */
|
|
27
|
+
function callHost(ctx, method, payload) {
|
|
28
|
+
return ctx.apiProxy.host[method]({ rpcId: 'mobile', method: `host.${method}`, payload });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 调宿主 apiProxy.workspace.*(工作区清单与采纳)。 */
|
|
32
|
+
function callWorkspace(ctx, method, payload) {
|
|
33
|
+
return ctx.apiProxy.workspace[method]({ rpcId: 'mobile', method: `workspace.${method}`, payload });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* GET /api/mobile/v1/workspaces(token)
|
|
38
|
+
* 列出被控端当前所有工作区,供侧边栏「工作区」区块渲染与重命名/删除判定。
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* 构建工作区快照 body(供移动端镜像同步推送复用)。
|
|
42
|
+
*
|
|
43
|
+
* `archivedSessionIds` 必须原样透传:宿主 `workspace.list` 一直在返回它,
|
|
44
|
+
* 但此前这里只保留 `items`,把它整个丢掉。于是 App 点完「归档会话」拿不到任何
|
|
45
|
+
* 归档状态,侧边栏那一行永远不消失——归档在移动端表现为一次没有反馈的空操作。
|
|
46
|
+
* 它同时也是将来做「取消归档」入口的唯一数据来源。
|
|
47
|
+
*/
|
|
48
|
+
async function buildWorkspacesBody(ctx) {
|
|
49
|
+
const { value, error } = unwrap(await callWorkspace(ctx, 'list', {}));
|
|
50
|
+
if (error) return { ok: false, error };
|
|
51
|
+
// 宿主报「成功」却没有给出清单数组,这是**没取到**,不是「你没有工作区」。
|
|
52
|
+
// 旧实现兜底成 `{ ok:true, items:[] }`,于是空篮子一路通过 workspace-guard(只判 .ok)、
|
|
53
|
+
// 被中转冻结成权威空清单、App 下一次 15s 轮询把好数据整个覆盖掉且全程零报错——
|
|
54
|
+
// 真机表现就是侧边栏只剩一根设备行、点不开、只有下拉刷新能救。
|
|
55
|
+
// 形状不对必须走错误通道:下游(App 保留旧数据、镜像保持上一份好快照)才能正确反应。
|
|
56
|
+
const items = Array.isArray(value) ? value : (value && Array.isArray(value.items) ? value.items : null);
|
|
57
|
+
if (items === null) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: {
|
|
61
|
+
code: 'host-workspace-list-degraded',
|
|
62
|
+
message: '宿主工作区清单形状异常:响应里没有 items 数组',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const archivedSessionIds = value && Array.isArray(value.archivedSessionIds)
|
|
67
|
+
? value.archivedSessionIds.filter(id => typeof id === 'string')
|
|
68
|
+
: [];
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
items,
|
|
72
|
+
archivedSessionIds,
|
|
73
|
+
// 与 sessions 快照同款的新鲜度/空态契约(见 lib/mobile/sessions.js 的说明):
|
|
74
|
+
// App 把 generatedAt + revision 存进快照用于展示「数据时间」,emptyConfirmed
|
|
75
|
+
// 让「用户真的删光了工作区」不必再靠连续两次空去猜。
|
|
76
|
+
generatedAt: Date.now(),
|
|
77
|
+
revision: listRevision(items),
|
|
78
|
+
emptyConfirmed: items.length === 0,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleWorkspaces(ctx, _req, res) {
|
|
83
|
+
sendJson(res, 200, await buildWorkspacesBody(ctx));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* GET /api/mobile/v1/directory/list?path=…(token;path 缺省 = 被控端主目录)
|
|
88
|
+
* 透传宿主 host.listDirectory:返回 { path, home, crumbs, entries, truncated },
|
|
89
|
+
* 供前端目录浏览器渲染面包屑与文件夹行(Show hidden 开关在前端侧边过滤)。
|
|
90
|
+
*/
|
|
91
|
+
async function handleListDirectory(ctx, req, res) {
|
|
92
|
+
const url = new URL(req.url, 'http://x');
|
|
93
|
+
const path = url.searchParams.get('path');
|
|
94
|
+
const payload = path && path.trim() ? { path: path.trim() } : {};
|
|
95
|
+
const { value, error } = unwrap(await callHost(ctx, 'listDirectory', payload));
|
|
96
|
+
if (error) return sendJson(res, 400, { ok: false, error });
|
|
97
|
+
sendJson(res, 200, { ok: true, listing: value });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* POST /api/mobile/v1/directory/create(token){ path, name }
|
|
102
|
+
* 透传宿主 host.createDirectory:在被控端 `path` 下新建名为 `name` 的文件夹。
|
|
103
|
+
* name 必须为单个非空路径段(不允许含 / 或 \\)。
|
|
104
|
+
*/
|
|
105
|
+
async function handleCreateDirectory(ctx, req, res) {
|
|
106
|
+
const body = await readJsonBody(req, 64 * 1024);
|
|
107
|
+
const path = body && typeof body.path === 'string' ? body.path.trim() : '';
|
|
108
|
+
const name = body && typeof body.name === 'string' ? body.name.trim() : '';
|
|
109
|
+
if (!path || !name) return sendJson(res, 400, { ok: false, error: { code: 'bad-request', message: '缺少 path 或 name' } });
|
|
110
|
+
const { value, error } = unwrap(await callHost(ctx, 'createDirectory', { path, name }));
|
|
111
|
+
if (error) return sendJson(res, 400, { ok: false, error });
|
|
112
|
+
sendJson(res, 200, { ok: true, path: value && value.path });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* POST /api/mobile/v1/workspace/create(token){ path }
|
|
117
|
+
* 透传宿主 workspace.create:把被控端 `path` 目录采纳为一个工作区(已存在则返回现有项)。
|
|
118
|
+
* 返回 { workspace: { workspaceId, path, title, ... }, created }。
|
|
119
|
+
*/
|
|
120
|
+
async function handleCreateWorkspace(ctx, req, res) {
|
|
121
|
+
const body = await readJsonBody(req, 64 * 1024);
|
|
122
|
+
const path = body && typeof body.path === 'string' ? body.path.trim() : '';
|
|
123
|
+
if (!path) return sendJson(res, 400, { ok: false, error: { code: 'bad-request', message: '缺少 path' } });
|
|
124
|
+
const { value, error } = unwrap(await callWorkspace(ctx, 'create', { path }));
|
|
125
|
+
if (error) return sendJson(res, 400, { ok: false, error });
|
|
126
|
+
sendJson(res, 200, { ok: true, workspace: value && value.workspace, created: !!(value && value.created) });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* POST /api/mobile/v1/workspace/delete(token){ workspaceId }
|
|
131
|
+
* 透传宿主 workspace.delete:删除工作区登记,但保留目录和会话日志。
|
|
132
|
+
*/
|
|
133
|
+
async function handleDeleteWorkspace(ctx, req, res) {
|
|
134
|
+
const body = await readJsonBody(req, 64 * 1024);
|
|
135
|
+
const workspaceId = body && typeof body.workspaceId === 'string' ? body.workspaceId.trim() : '';
|
|
136
|
+
if (!workspaceId) {
|
|
137
|
+
return sendJson(res, 400, { ok: false, error: { code: 'bad-request', message: '需要 workspaceId' } });
|
|
138
|
+
}
|
|
139
|
+
const { value, error } = unwrap(await callWorkspace(ctx, 'delete', { workspaceId }));
|
|
140
|
+
if (error) return sendJson(res, 400, { ok: false, error });
|
|
141
|
+
sendJson(res, 200, { ok: true, ...(value || { deleted: true }) });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* POST /api/mobile/v1/workspace/move-session(token){ workspaceId, sessionId, beforeSessionId? }
|
|
146
|
+
* 透传宿主 workspace.insertSessionBefore,使移动端和桌面端共享工作区排序。
|
|
147
|
+
*/
|
|
148
|
+
async function handleMoveSession(ctx, req, res) {
|
|
149
|
+
const body = await readJsonBody(req, 64 * 1024);
|
|
150
|
+
const workspaceId = body && typeof body.workspaceId === 'string' ? body.workspaceId.trim() : '';
|
|
151
|
+
const sessionId = body && typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
152
|
+
const beforeSessionId = body && typeof body.beforeSessionId === 'string' ? body.beforeSessionId.trim() : '';
|
|
153
|
+
if (!workspaceId || !sessionId) {
|
|
154
|
+
return sendJson(res, 400, { ok: false, error: { code: 'bad-request', message: '需要 workspaceId 和 sessionId' } });
|
|
155
|
+
}
|
|
156
|
+
const payload = { workspaceId, sessionId, ...(beforeSessionId ? { beforeSessionId } : {}) };
|
|
157
|
+
const { value, error } = unwrap(await callWorkspace(ctx, 'insertSessionBefore', payload));
|
|
158
|
+
if (error) return sendJson(res, 400, { ok: false, error });
|
|
159
|
+
invalidateSessionList(ctx);
|
|
160
|
+
sendJson(res, 200, { ok: true, workspace: value && value.workspace });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* POST /api/mobile/v1/workspace/rename(token){ workspaceId, title }
|
|
165
|
+
* 透传宿主 workspace.rename:改工作区显示名,重名返回 workspace/name-conflict。
|
|
166
|
+
* 返回 { workspace }。App 的 `renameWorkspace()` 一直在调这个地址,但在本次修复
|
|
167
|
+
* 之前插件根本没注册它 —— 会话列表里的「重命名工作区」永远 404。
|
|
168
|
+
*/
|
|
169
|
+
async function handleRenameWorkspace(ctx, req, res) {
|
|
170
|
+
const body = await readJsonBody(req, 64 * 1024);
|
|
171
|
+
const workspaceId = body && typeof body.workspaceId === 'string' ? body.workspaceId.trim() : '';
|
|
172
|
+
const title = body && typeof body.title === 'string' ? body.title.trim() : '';
|
|
173
|
+
if (!workspaceId || !title) {
|
|
174
|
+
return sendJson(res, 400, { ok: false, error: { code: 'bad-request', message: '需要 workspaceId 和 title' } });
|
|
175
|
+
}
|
|
176
|
+
const { value, error } = unwrap(await callWorkspace(ctx, 'rename', { workspaceId, title }));
|
|
177
|
+
if (error) return sendJson(res, 400, { ok: false, error });
|
|
178
|
+
sendJson(res, 200, { ok: true, workspace: value && value.workspace });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* POST /api/mobile/v1/workspace/archive-session(token){ sessionId, stopActivity? }
|
|
183
|
+
* 透传宿主 workspace.archiveSession:把会话移入归档(日志保留、从工作区列表隐藏)。
|
|
184
|
+
* 这是移动端唯一的「删除会话」语义,同样曾因未注册路由而对 App 返回 404。
|
|
185
|
+
*/
|
|
186
|
+
async function handleArchiveSession(ctx, req, res) {
|
|
187
|
+
const body = await readJsonBody(req, 64 * 1024);
|
|
188
|
+
const sessionId = body && typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
189
|
+
if (!sessionId) {
|
|
190
|
+
return sendJson(res, 400, { ok: false, error: { code: 'bad-request', message: '需要 sessionId' } });
|
|
191
|
+
}
|
|
192
|
+
// 归档在宿主侧本身是幂等的(重复归档同一会话状态不变),但仍按 requestId 重放:
|
|
193
|
+
// 重投要拿到和第一次一样的结果,而不是让 App 再多等一次 RPC。
|
|
194
|
+
const outcome = await withIdempotency(ctx, canonicalDeviceKey(req), requestIdOf(req, body), async () => {
|
|
195
|
+
const payload = { sessionId, ...(body.stopActivity === true ? { stopActivity: true } : {}) };
|
|
196
|
+
const { value, error } = unwrap(await callWorkspace(ctx, 'archiveSession', payload));
|
|
197
|
+
if (error) return { status: 400, body: { ok: false, error } };
|
|
198
|
+
invalidateSessionList(ctx);
|
|
199
|
+
return { status: 200, body: { ok: true, ...(value || {}) } };
|
|
200
|
+
});
|
|
201
|
+
sendJson(res, outcome.status, outcome.body);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
buildWorkspacesBody,
|
|
206
|
+
handleWorkspaces,
|
|
207
|
+
handleListDirectory,
|
|
208
|
+
handleCreateDirectory,
|
|
209
|
+
handleCreateWorkspace,
|
|
210
|
+
handleRenameWorkspace,
|
|
211
|
+
handleDeleteWorkspace,
|
|
212
|
+
handleMoveSession,
|
|
213
|
+
handleArchiveSession,
|
|
214
|
+
};
|