dsh-mobile-plugin 0.5.21 → 0.5.23

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 (41) hide show
  1. package/README.md +3 -12
  2. package/assets/pets/README.md +9 -0
  3. package/assets/pets/deepy-whale/attention.gif +0 -0
  4. package/assets/pets/deepy-whale/done.gif +0 -0
  5. package/assets/pets/deepy-whale/error.gif +0 -0
  6. package/assets/pets/deepy-whale/idle.gif +0 -0
  7. package/assets/pets/deepy-whale/pack.json +54 -0
  8. package/assets/pets/deepy-whale/sleeping.gif +0 -0
  9. package/assets/pets/deepy-whale/working-3.gif +0 -0
  10. package/assets/pets/deepy-whale/working.gif +0 -0
  11. package/extensions/README.md +64 -0
  12. package/extensions/catalog.json +1 -0
  13. package/lib/impl.js +21 -1
  14. package/lib/mobile/approvals.js +22 -2
  15. package/lib/mobile/cli/acp.js +34 -20
  16. package/lib/mobile/cli/mobile.js +8 -1
  17. package/lib/mobile/cli/service.js +151 -10
  18. package/lib/mobile/extension-catalog.js +244 -0
  19. package/lib/mobile/live-steps.js +35 -3
  20. package/lib/mobile/local-images.js +166 -87
  21. package/lib/mobile/pet-tools.js +215 -0
  22. package/lib/mobile/pets.js +559 -0
  23. package/lib/mobile/session-local-image.js +9 -1
  24. package/lib/mobile/stats-collect.cjs +15 -7
  25. package/lib/mobile/stats-ledger.cjs +44 -5
  26. package/lib/mobile/stream.js +8 -5
  27. package/lib/mobile/wechat-tools.js +257 -0
  28. package/lib/mobile/wechat.js +489 -0
  29. package/lib/panel/index.html +10 -10
  30. package/lib/relay/pairing-payload.cjs +13 -5
  31. package/lib/relay/snapshot-auth.js +82 -0
  32. package/lib/relay/status.js +3 -0
  33. package/lib/routes/control-plane.js +65 -3
  34. package/lib/routes/mobile-api.js +54 -1
  35. package/lib/sidecar-facade-worker.cjs +8 -0
  36. package/lib/tailcat/host-process.cjs +270 -0
  37. package/lib/tailcat/ingress.cjs +98 -0
  38. package/lib/tailcat/session.cjs +264 -0
  39. package/lib/tailcat/wiring.cjs +96 -0
  40. package/lib/update-install.cjs +43 -0
  41. package/package.json +10 -1
@@ -0,0 +1,215 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 给模型的两个工具:看一眼这台电脑上有哪些宠物包、做一只新的(或者改现有的)。
5
+ *
6
+ * ## 为什么要有工具
7
+ *
8
+ * 宠物包是「一个目录 + 一份 pack.json + 几张图」,而素材基本都已经是电脑上的图片文件了。
9
+ * 让模型自己往目录里写,它记不住放哪儿、id 什么形状、状态键叫什么;写完还得自己回一句
10
+ * 「好了」,用户那边刷半天也不一定看得到。工具负责的正是这几件它记不住的事。
11
+ *
12
+ * ## 为什么不收 base64 为主
13
+ *
14
+ * 与公众号那条同一个理由:让模型把几 MB 的图抄进工具参数,抄不动也抄不对。它手里真有的是
15
+ * 「那张图在这台电脑上的路径」,所以 `filePath` 是主路,`base64` 只在它手上真有字节时兜底。
16
+ *
17
+ * ## 能力面的边界
18
+ *
19
+ * 只碰这个库:id 形状卡死、图片只认单层 `.gif` / `.png`、单张与整包都有上限。**没有删除** ——
20
+ * 删是人的动作(设置页那边有个按钮),不该由模型顺手做掉。
21
+ */
22
+
23
+ const { defineTool } = require('@deepseek-ai/dsh-tools');
24
+
25
+ const {
26
+ listPacks,
27
+ writePack,
28
+ isImageName,
29
+ ID_RE,
30
+ KNOWN_STATES,
31
+ } = require('./pets.js');
32
+
33
+ /** 列表里一行一只:名字、id、内置还是自己做的、状态齐不齐。 */
34
+ function packLine(pack) {
35
+ const tags = [pack.builtin ? '内置' : '自己做的'];
36
+ tags.push(`id ${pack.id}`);
37
+ // `pack.states` 到这一层已经是状态名数组(execute 那步折过),按名字查键永远是 undefined。
38
+ const missing = KNOWN_STATES.filter((state) => state !== 'sleeping' && !pack.states.includes(state));
39
+ tags.push(missing.length === 0 ? '五个状态都有' : `缺 ${missing.join(' / ')}(将回退到 idle)`);
40
+ if (pack.author) tags.push(pack.author);
41
+ return `· 《${pack.name}》${tags.join(',')}`;
42
+ }
43
+
44
+ /**
45
+ * 注册这两个工具。
46
+ *
47
+ * 必须走 `ctx.inject(['tools'], …)`:宿主里 `tools` 是子代理作用域提供的服务,插件这一层
48
+ * 直接读 `ctx.tools` 拿到的常常是 undefined(见 `phone/tools.js` 那一整段教训)。缺失时
49
+ * 静默跳过,别把整块装配掀掉。
50
+ */
51
+ function installPetTools(ctx) {
52
+ if (typeof ctx.inject !== 'function') return;
53
+ try {
54
+ ctx.inject(['tools'], mountTools);
55
+ } catch (error) {
56
+ // 没有 tools 注入的 profile(cordis 抛 "… without inject"):只少这两个工具,包目录与
57
+ // 三条路由照常可用。其余错误照抛 —— 别把真的注册失败也当成「这个 profile 没注入」。
58
+ const message = error && error.message ? error.message : String(error);
59
+ if (!message.includes('without inject')) throw error;
60
+ }
61
+ }
62
+
63
+ /** `ctx.inject` 的回调体。单独拎出来只为让上面那层 try/catch 短一点。 */
64
+ function mountTools(scoped) {
65
+ if (!scoped || !scoped.tools || typeof scoped.tools.register !== 'function') return;
66
+
67
+ scoped.tools.register(defineTool({
68
+ name: 'pet_pack_list',
69
+ description: 'List the desktop-pet packages on this computer: the built-in ones that ship with the app, plus any the user made. Each line gives the id, name, whether it is built-in, and which animation states it has. Use this when the user asks what pets they have or what their pet looks like, and before writing a pack so you pick an id that is free.',
70
+ parameters: {},
71
+ output: {
72
+ schema: {
73
+ type: 'object',
74
+ additionalProperties: false,
75
+ properties: {
76
+ packs: {
77
+ type: 'array',
78
+ required: true,
79
+ items: {
80
+ type: 'object',
81
+ additionalProperties: false,
82
+ properties: {
83
+ id: { type: 'string' },
84
+ name: { type: 'string' },
85
+ builtin: { type: 'boolean' },
86
+ states: { type: 'array', items: { type: 'string' } },
87
+ },
88
+
89
+ },
90
+ },
91
+ },
92
+ },
93
+ render: (_args, value) => {
94
+ const packs = Array.isArray(value.packs) ? value.packs : [];
95
+ return [{
96
+ type: 'text',
97
+ text: packs.length === 0
98
+ ? '这台电脑上一个宠物包都还没有。用户想做一只的话,用 pet_pack_write 建一个。'
99
+ : `这台电脑上有 ${packs.length} 只:\n${packs.map(packLine).join('\n')}`,
100
+ }];
101
+ },
102
+ },
103
+ execute: async () => ({
104
+ packs: listPacks().map((pack) => ({
105
+ id: pack.id,
106
+ name: pack.name,
107
+ builtin: pack.builtin,
108
+ states: Object.keys(pack.states),
109
+ })),
110
+ }),
111
+ }));
112
+
113
+ scoped.tools.register(defineTool({
114
+ name: 'pet_pack_write',
115
+ description: 'Create or replace a desktop-pet package on this computer, so it shows up on the user\'s phone under Settings → floating pet. Use this whenever the user wants to change their pet, make a new one, or drop in their own artwork. A pack is one directory with a pack.json plus the images it references. States are idle / working / attention / done / error / sleeping, and idle is required; a state you leave out falls back to idle. Pictures are GIF (animated) or PNG (static). Give each picture the PATH of the file on this computer — the host reads it — rather than its bytes; base64 is only for when you already hold the bytes. Existing packs with the same id are replaced wholesale, including a built-in one (warn the user before doing that). You cannot delete packs; that is the user\'s job in Settings.',
116
+ parameters: {
117
+ id: {
118
+ type: 'string',
119
+ description: 'Pack id, also the folder name: 1-64 lowercase letters, digits and dashes, not starting with a dash, e.g. deepy-whale.',
120
+ },
121
+ pack: {
122
+ type: 'object',
123
+ additionalProperties: true,
124
+ description: 'The whole pack.json as an object. Required keys: name and states (with at least idle). Optional: author, license, canvas {w,h}, anchor {x,baseline}, tiers.working [{min,file}] (largest min wins), loop [], sleepAfterMs. Every file named here must be one you pass in files[].',
125
+ properties: {
126
+ name: { type: 'string', description: 'Shown in the picker on the phone.' },
127
+ author: { type: 'string', description: 'Credited in the picker footer. Fill it in whenever the artwork is not yours.' },
128
+ license: { type: 'string', description: 'License note, shown next to the author.' },
129
+ states: {
130
+ type: 'object',
131
+ additionalProperties: true,
132
+ description: 'State name → { file }. Recognised: idle, working, attention, done, error, sleeping. At least idle is required.',
133
+ },
134
+ tiers: {
135
+ type: 'object',
136
+ additionalProperties: true,
137
+ description: 'Optional. tiers.working is [{min, file}]: pick the image by how many conversations are running.',
138
+ },
139
+ loop: { type: 'array', items: { type: 'string' }, description: 'States that loop. Defaults to idle, working, attention.' },
140
+ sleepAfterMs: { type: 'number', description: 'Idle time before switching to sleeping. Only meaningful when the sleeping state exists.' },
141
+ },
142
+ },
143
+ files: {
144
+ type: 'array',
145
+ description: 'The images this pack uses. Each one is written into the pack folder under that name.',
146
+ items: {
147
+ type: 'object',
148
+ additionalProperties: false,
149
+ properties: {
150
+ path: { type: 'string', required: true, description: 'File name inside the pack, e.g. idle.gif. Single level, .gif or .png, no slashes.' },
151
+ filePath: { type: 'string', description: 'Absolute path of the picture on this computer. This is the normal way.' },
152
+ base64: { type: 'string', description: 'The image bytes, base64 encoded. Only when you already hold the bytes and have no file to point at.' },
153
+ },
154
+ },
155
+ },
156
+ },
157
+ output: {
158
+ schema: {
159
+ type: 'object',
160
+ additionalProperties: false,
161
+ properties: {
162
+ id: { type: 'string', required: true },
163
+ name: { type: 'string', required: true },
164
+ states: { type: 'array', items: { type: 'string' } },
165
+ written: { type: 'array', items: { type: 'string' } },
166
+ note: { type: 'string' },
167
+ },
168
+ },
169
+ render: (_args, value) => {
170
+ const lines = [`《${value.name}》存好了,id ${value.id},状态:${(value.states || []).join(' / ') || 'idle'}。`];
171
+ if (Array.isArray(value.written) && value.written.length > 0) lines.push(`图片已存:${value.written.join('、')}。`);
172
+ if (value.note) lines.push(value.note);
173
+ return [{ type: 'text', text: lines.join('\n') }];
174
+ },
175
+ },
176
+ execute: async (args) => {
177
+ const id = typeof args.id === 'string' ? args.id.trim() : '';
178
+ if (!ID_RE.test(id)) {
179
+ throw new Error(`"${id}" 不是合法的宠物包 id:只认 1-64 位小写字母、数字与连字符,且不以连字符开头。`);
180
+ }
181
+ // 文件名先收窄一遍再交给库:这样「哪几个名字不合规矩」能在写之前就一趟说清。
182
+ const files = Array.isArray(args.files) ? args.files : [];
183
+ const badNames = files
184
+ .map((item) => (item && typeof item.path === 'string' ? item.path.trim() : ''))
185
+ .filter((name) => !isImageName(name));
186
+ if (badNames.length > 0) {
187
+ throw new Error(`这几个文件名不合规矩:${badNames.map((name) => `"${name}"`).join('、')}。只认单层 .gif / .png(不含斜杠),例如 idle.gif。`);
188
+ }
189
+
190
+ let result;
191
+ try {
192
+ result = await writePack({ id, pack: args.pack, files });
193
+ } catch (error) {
194
+ throw new Error(error && error.message ? error.message : '没能写进去。');
195
+ }
196
+ const { pack, rejected, written, overriddenBuiltin } = result;
197
+ const notes = [];
198
+ if (overriddenBuiltin) {
199
+ notes.push(`这个 id 原本有一只内置的宠物,现在被这份盖住了(手机上显示的是新的这只)。内置那份没被改动。`);
200
+ }
201
+ if (rejected.length > 0) {
202
+ notes.push(`这几张没能收进来:${rejected.join('、')}(读不到,或者不是 .gif / .png)。`);
203
+ }
204
+ return {
205
+ id: pack.id,
206
+ name: pack.name,
207
+ states: Object.keys(pack.states),
208
+ written,
209
+ ...(notes.length > 0 ? { note: notes.join('\n') } : {}),
210
+ };
211
+ },
212
+ }));
213
+ }
214
+
215
+ module.exports = { installPetTools };