@sjhmars/happy-bridge 0.3.0 → 0.3.1
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 +2 -2
- package/lib/client.js +9 -9
- package/lib/index.js +65 -20
- package/lib/types/inbound.d.ts +25 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
- 新建:只能选已经在网页登记过的工作区。不会在磁盘上 mkdir。App 里「new worktree」会失败(故意不暴露 bash)。
|
|
22
22
|
- 发文字:已注册 `/命令` 走命令通道;否则当聊天。`/skill名` 仍当普通消息,由 harness 注入。
|
|
23
23
|
- 发图片:相册/拍照/粘贴后,会随下一条文字(也可以只发图)进同一场电脑对话,模型能看见图。`.java` / `.txt` 等普通文件会先下载到工作区 `happy-inbox/`,再走电脑原来的 `read` 工具读内容(和你在仓库里打开一个文件一样)。PDF、Word 这类不是纯文本的文件,电脑的 `read` 会拒绝,插件也不会自己拆文档。远程档至少「能聊」。输入框左边应有图片按钮;发出后电脑网页会出现图片,手机会转圈等回复。电脑网页贴的图也会按同一套加密上传推到手机。
|
|
24
|
-
- 工具批准、计划审阅、选择题:和网页黄框/提问框赛跑,谁先点谁算。手机点「始终允许」只在这次网页进程里记住。选择题走 Happy
|
|
24
|
+
- 工具批准、计划审阅、选择题:和网页黄框/提问框赛跑,谁先点谁算。手机点「始终允许」只在这次网页进程里记住。选择题走 Happy 表单通道,每道题末尾多一个「✏️ 自定义…」选项:点它之后在输入框写出你的想法直接发送,那条文本就是这道题的答案(其他题已选的项保留);答完后卡片会显示所选内容和自定义文本。旧版 App 没有这条通道,见 `questionChannel`。提问还在等的时候,直接用输入框打字也等于自己填;审阅计划时打字会关掉审阅,这句话当普通聊天发出去。批完/拒完的卡片会停在「已批准/已拒绝」状态,按钮失效。远程档至少「能批」才能从手机点。
|
|
25
25
|
- 电脑正在生成时,手机可以点停止(Happy App 的 Stop)。远程档至少「能聊」。只看档点了不会停。
|
|
26
26
|
- 助手回复按整段推到手机(生成中会转圈)。思考收成一行 **Think · 摘要** 的 Note 卡片,点进去看全文。工具用 Happy 认识的名字(Grep / Read / Bash),描述跟网页那一行摘要一样。Happy 没有网页同款图标。
|
|
27
27
|
|
|
@@ -74,7 +74,7 @@ pnpm dsh web --port 3080
|
|
|
74
74
|
|
|
75
75
|
`remoteGrant`:`watch` | `chat` | `approve` | `full`。设置页下拉框会写入这份配置。会话里也可以 `/remote approve`(只改当前进程,不写设置)。档位「完整」时,手机换模型和思考强度走电脑同一条 `session.selectModel`,网页模型栏和推理等级会跟着变。电脑自己换模型和思考强度同样会写回手机。网页加了新模型后,已镜像的手机会话会重新收到完整名单。
|
|
76
76
|
|
|
77
|
-
`questionChannel`:`communications`(默认)| `permission`。选择题走的 Happy 通道。`communications` 是 App
|
|
77
|
+
`questionChannel`:`communications`(默认)| `permission`。选择题走的 Happy 通道。`communications` 是 App 的表单通道:答完卡片常驻显示你选的答案,会话列表也会显示「等回答」;需要较新版 Happy App。`permission` 是旧通道:题目选项仍可见可答,答完卡片不回显。两条通道都没有内联输入框——自由回答都靠每题末尾的「✏️ 自定义…」选项 + 输入框打字(`communications` 下,零选项的题目会弹 App 全屏表单,那里有真正的输入框)。
|
|
78
78
|
|
|
79
79
|
## 已知限制
|
|
80
80
|
|
package/lib/client.js
CHANGED
|
@@ -17,21 +17,21 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
document.head.appendChild(tag);
|
|
18
18
|
}
|
|
19
19
|
var HappyBridgeCard_module_css_default = {
|
|
20
|
+
"name": "l9rBEq_name",
|
|
20
21
|
"body": "l9rBEq_body",
|
|
21
|
-
"
|
|
22
|
+
"qr": "l9rBEq_qr",
|
|
22
23
|
"description": "l9rBEq_description",
|
|
23
|
-
"
|
|
24
|
+
"select": "l9rBEq_select",
|
|
25
|
+
"card": "l9rBEq_card",
|
|
24
26
|
"headText": "l9rBEq_headText",
|
|
27
|
+
"status": "l9rBEq_status",
|
|
28
|
+
"row": "l9rBEq_row",
|
|
29
|
+
"header": "l9rBEq_header",
|
|
25
30
|
"chevron": "l9rBEq_chevron",
|
|
26
|
-
"
|
|
27
|
-
"card": "l9rBEq_card",
|
|
31
|
+
"checkbox": "l9rBEq_checkbox",
|
|
28
32
|
"error": "l9rBEq_error",
|
|
29
33
|
"button": "l9rBEq_button",
|
|
30
|
-
"
|
|
31
|
-
"checkbox": "l9rBEq_checkbox",
|
|
32
|
-
"label": "l9rBEq_label",
|
|
33
|
-
"select": "l9rBEq_select",
|
|
34
|
-
"header": "l9rBEq_header"
|
|
34
|
+
"label": "l9rBEq_label"
|
|
35
35
|
};
|
|
36
36
|
//#endregion
|
|
37
37
|
//#region src/client/HappyBridgeCard.tsx
|
package/lib/index.js
CHANGED
|
@@ -1699,19 +1699,6 @@ function answersFromHappy(answers, questions) {
|
|
|
1699
1699
|
});
|
|
1700
1700
|
}
|
|
1701
1701
|
/**
|
|
1702
|
-
* Fill every still-open question with the typed chat line as `custom`.
|
|
1703
|
-
* @param questions - harness questions in order.
|
|
1704
|
-
* @param text - the App composer line.
|
|
1705
|
-
* @returns one answer row per question.
|
|
1706
|
-
*/
|
|
1707
|
-
function customAnswersFromText(questions, text) {
|
|
1708
|
-
return questions.map((question) => ({
|
|
1709
|
-
id: question.id,
|
|
1710
|
-
selected: [],
|
|
1711
|
-
custom: text
|
|
1712
|
-
}));
|
|
1713
|
-
}
|
|
1714
|
-
/**
|
|
1715
1702
|
* The option label that declines a plan-review question.
|
|
1716
1703
|
* @param question - first question of a plan-review `ask`.
|
|
1717
1704
|
* @returns the non-approve option label, or Keep planning.
|
|
@@ -1744,6 +1731,41 @@ function parsePermissionRpc(params) {
|
|
|
1744
1731
|
return result;
|
|
1745
1732
|
}
|
|
1746
1733
|
/**
|
|
1734
|
+
* Synthetic last option the bridge appends to every question: the App's inline
|
|
1735
|
+
* form has no free-text input, so "custom answer" is a two-tap flow — pick
|
|
1736
|
+
* this option, then type the real answer in the composer.
|
|
1737
|
+
*/
|
|
1738
|
+
const CUSTOM_ANSWER_LABEL = "✏️ 自定义…";
|
|
1739
|
+
/**
|
|
1740
|
+
* Merge composer text with a deferred (marker-picked) submission.
|
|
1741
|
+
* - No deferred selections: the text becomes `custom` for every question
|
|
1742
|
+
* (the original "type while waiting" behavior).
|
|
1743
|
+
* - With deferred selections: real options are kept per question; questions
|
|
1744
|
+
* that picked the marker — and questions never answered — take the text
|
|
1745
|
+
* as `custom`.
|
|
1746
|
+
* @param questions - harness questions in order.
|
|
1747
|
+
* @param text - the composer line.
|
|
1748
|
+
* @param deferred - per-question submitted options, keyed by question id.
|
|
1749
|
+
* @returns one answer row per question.
|
|
1750
|
+
*/
|
|
1751
|
+
function mergeCustomAnswers(questions, text, deferred) {
|
|
1752
|
+
return questions.map((question) => {
|
|
1753
|
+
const selected = deferred?.[question.id];
|
|
1754
|
+
if (selected === void 0) return {
|
|
1755
|
+
id: question.id,
|
|
1756
|
+
selected: [],
|
|
1757
|
+
custom: text
|
|
1758
|
+
};
|
|
1759
|
+
const real = selected.filter((option) => option !== CUSTOM_ANSWER_LABEL);
|
|
1760
|
+
const marked = real.length !== selected.length;
|
|
1761
|
+
return {
|
|
1762
|
+
id: question.id,
|
|
1763
|
+
selected: real,
|
|
1764
|
+
...!marked && real.length > 0 ? {} : { custom: text }
|
|
1765
|
+
};
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1747
1769
|
* Translate Happy communications answers (`{ [question id]: { options, custom } }`)
|
|
1748
1770
|
* into harness answer rows. Options become `selected`; free text becomes `custom`.
|
|
1749
1771
|
* @param answers - communication RPC answers keyed by question id.
|
|
@@ -3714,12 +3736,12 @@ var HappyBridge = class {
|
|
|
3714
3736
|
const files = await this.drainPhoneFiles(link);
|
|
3715
3737
|
const pending = link.pendingHuman;
|
|
3716
3738
|
if (pending?.kind === "ask") {
|
|
3717
|
-
const answers =
|
|
3739
|
+
const answers = mergeCustomAnswers(pending.questions, message.text, pending.deferred);
|
|
3718
3740
|
pending.resolve({ answers });
|
|
3719
3741
|
delete link.pendingHuman;
|
|
3720
3742
|
if (pending.communication !== void 0) this.completeCommunication(link, pending.id, pending.communication, "answered", Object.fromEntries(answers.map((row) => [row.id, {
|
|
3721
|
-
options:
|
|
3722
|
-
custom: row.custom
|
|
3743
|
+
options: row.selected,
|
|
3744
|
+
...row.custom === void 0 ? {} : { custom: row.custom }
|
|
3723
3745
|
}])));
|
|
3724
3746
|
else this.clearRequest(link, pending.id, "canceled", pending.request);
|
|
3725
3747
|
link.socket.sendToolEnd(pending.id);
|
|
@@ -4149,10 +4171,10 @@ var HappyBridge = class {
|
|
|
4149
4171
|
id: question.id,
|
|
4150
4172
|
header: question.header ?? question.question.slice(0, 24),
|
|
4151
4173
|
question: question.question,
|
|
4152
|
-
options: (question.options ?? []).map((option) => ({
|
|
4174
|
+
options: [...(question.options ?? []).map((option) => ({
|
|
4153
4175
|
label: option.label,
|
|
4154
4176
|
...option.description === void 0 ? {} : { description: option.description }
|
|
4155
|
-
})),
|
|
4177
|
+
})), ...(question.options ?? []).length > 0 ? [{ label: CUSTOM_ANSWER_LABEL }] : []],
|
|
4156
4178
|
multiSelect: question.multiSelect === true,
|
|
4157
4179
|
allowCustom: true,
|
|
4158
4180
|
required: true
|
|
@@ -4184,10 +4206,10 @@ var HappyBridge = class {
|
|
|
4184
4206
|
const arguments_ = { questions: request.questions.map((question) => ({
|
|
4185
4207
|
question: question.question,
|
|
4186
4208
|
header: question.header ?? question.question.slice(0, 24),
|
|
4187
|
-
options: (question.options ?? []).map((option) => ({
|
|
4209
|
+
options: [...(question.options ?? []).map((option) => ({
|
|
4188
4210
|
label: option.label,
|
|
4189
4211
|
description: option.description ?? ""
|
|
4190
|
-
})),
|
|
4212
|
+
})), { label: CUSTOM_ANSWER_LABEL }],
|
|
4191
4213
|
multiSelect: question.multiSelect === true
|
|
4192
4214
|
})) };
|
|
4193
4215
|
link.pendingHuman = {
|
|
@@ -4273,6 +4295,17 @@ var HappyBridge = class {
|
|
|
4273
4295
|
return;
|
|
4274
4296
|
}
|
|
4275
4297
|
const mapped = answersFromHappy(rpc.updatedInput?.answers, pending.questions);
|
|
4298
|
+
const deferred = {};
|
|
4299
|
+
let wantsCustom = false;
|
|
4300
|
+
for (const row of mapped) {
|
|
4301
|
+
deferred[row.id] = row.selected;
|
|
4302
|
+
if (row.selected.includes("✏️ 自定义…")) wantsCustom = true;
|
|
4303
|
+
}
|
|
4304
|
+
if (wantsCustom) {
|
|
4305
|
+
pending.deferred = deferred;
|
|
4306
|
+
link.socket.sendText("service", "已选自定义回答:请在输入框输入你的想法,直接发送即可。");
|
|
4307
|
+
return;
|
|
4308
|
+
}
|
|
4276
4309
|
pending.resolve({ answers: mapped.map((row) => ({
|
|
4277
4310
|
id: row.id,
|
|
4278
4311
|
selected: row.selected
|
|
@@ -4297,6 +4330,18 @@ var HappyBridge = class {
|
|
|
4297
4330
|
link.socket.sendToolEnd(rpc.id);
|
|
4298
4331
|
return;
|
|
4299
4332
|
}
|
|
4333
|
+
const deferred = {};
|
|
4334
|
+
let wantsCustom = false;
|
|
4335
|
+
for (const [questionId, answer] of Object.entries(rpc.answers)) {
|
|
4336
|
+
deferred[questionId] = answer.options;
|
|
4337
|
+
if (answer.options.includes("✏️ 自定义…")) wantsCustom = true;
|
|
4338
|
+
}
|
|
4339
|
+
if (wantsCustom) {
|
|
4340
|
+
pending.deferred = deferred;
|
|
4341
|
+
this.completeCommunication(link, rpc.id, pending.communication, "answered", rpc.answers);
|
|
4342
|
+
link.socket.sendText("service", "已选自定义回答:请在输入框输入你的想法,直接发送即可。");
|
|
4343
|
+
return;
|
|
4344
|
+
}
|
|
4300
4345
|
pending.resolve({ answers: answersFromCommunication(rpc.answers, pending.questions) });
|
|
4301
4346
|
delete link.pendingHuman;
|
|
4302
4347
|
this.completeCommunication(link, rpc.id, pending.communication, "answered", rpc.answers);
|
package/lib/types/inbound.d.ts
CHANGED
|
@@ -85,6 +85,31 @@ export declare function planReviewDeclineLabel(question: {
|
|
|
85
85
|
* @returns id, approved flag, optional Always-allow decision, and answers.
|
|
86
86
|
*/
|
|
87
87
|
export declare function parsePermissionRpc(params: unknown): PermissionRpc;
|
|
88
|
+
/**
|
|
89
|
+
* Synthetic last option the bridge appends to every question: the App's inline
|
|
90
|
+
* form has no free-text input, so "custom answer" is a two-tap flow — pick
|
|
91
|
+
* this option, then type the real answer in the composer.
|
|
92
|
+
*/
|
|
93
|
+
export declare const CUSTOM_ANSWER_LABEL = "\u270F\uFE0F \u81EA\u5B9A\u4E49\u2026";
|
|
94
|
+
/**
|
|
95
|
+
* Merge composer text with a deferred (marker-picked) submission.
|
|
96
|
+
* - No deferred selections: the text becomes `custom` for every question
|
|
97
|
+
* (the original "type while waiting" behavior).
|
|
98
|
+
* - With deferred selections: real options are kept per question; questions
|
|
99
|
+
* that picked the marker — and questions never answered — take the text
|
|
100
|
+
* as `custom`.
|
|
101
|
+
* @param questions - harness questions in order.
|
|
102
|
+
* @param text - the composer line.
|
|
103
|
+
* @param deferred - per-question submitted options, keyed by question id.
|
|
104
|
+
* @returns one answer row per question.
|
|
105
|
+
*/
|
|
106
|
+
export declare function mergeCustomAnswers(questions: readonly {
|
|
107
|
+
id: string;
|
|
108
|
+
}[], text: string, deferred?: Record<string, string[]>): {
|
|
109
|
+
id: string;
|
|
110
|
+
selected: string[];
|
|
111
|
+
custom?: string;
|
|
112
|
+
}[];
|
|
88
113
|
/**
|
|
89
114
|
* Translate Happy communications answers (`{ [question id]: { options, custom } }`)
|
|
90
115
|
* into harness answer rows. Options become `selected`; free text becomes `custom`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjhmars/happy-bridge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Remote-control bridge: pair a running dsh web/desktop client with the Happy mobile app by QR code, then remote-control the same harness sessions from the phone (send messages, read replies, approve tool calls, switch models)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|