cortico-world-wechat 0.1.4 → 0.1.5
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/CHANGELOG.md +12 -0
- package/package.json +53 -53
- package/src/format.js +36 -20
- package/src/world.js +8 -4
- package/test/format.test.mjs +51 -33
- package/test/world.test.mjs +23 -16
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
本文件的 `## <版本号>` 段落会显示在扩展市场的发布时间线上(索引仓的 Action 按同样规则切段)。
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
## 0.1.5
|
|
7
|
+
|
|
8
|
+
- **转达口径(行为变更)**:推给桌宠的文本从「通知」改为「转达请求」。
|
|
9
|
+
此前她收到 `XX 发来消息:<原文>。跟胡闹说一声。` 会把原文当成对她说的、直接接着聊
|
|
10
|
+
(实测:原文「你在干嘛呢?」,她答「在呢在呢!」)。原因是问句本身在邀请回答,
|
|
11
|
+
而"转达"只是附加从句。现在把动作要求前置,并写明**别自己回答**。
|
|
12
|
+
- **分档转达**:正文不超过 `relayQuoteMax`(默认 40 字)时**照原文转达**,保证不失真;
|
|
13
|
+
超出、或没有文字正文时,改为只要求**一句话转述要点**,不搬运长文
|
|
14
|
+
(省上下文,也避免她复述长文时跑偏)。
|
|
15
|
+
- 新增配置项 `relayQuoteMax`,可调整分档阈值。
|
|
16
|
+
- 统一加 `【微信】` 前缀,便于识别与排查(此前只有长消息分支带前缀)。
|
|
17
|
+
|
|
6
18
|
## 0.1.4
|
|
7
19
|
|
|
8
20
|
- **补充作者字段 `author`**(`静音胡闹 <2310468741@qq.com>`),此前漏填,npm 页面上作者为空。
|
package/package.json
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "cortico-world-wechat",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"author": "静音胡闹 <2310468741@qq.com>",
|
|
5
|
-
"description": "把 Windows 微信新消息播报给桌宠:扩展自己解密并轮询微信本地库,把新消息推进桌宠的事件流,由桌宠决定怎么汇报。",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"main": "src/index.js",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": "./src/index.js",
|
|
11
|
-
"./package.json": "./package.json"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"src",
|
|
15
|
-
"tools",
|
|
16
|
-
"test",
|
|
17
|
-
"describe.json",
|
|
18
|
-
"CHANGELOG.md",
|
|
19
|
-
"README.md"
|
|
20
|
-
],
|
|
21
|
-
"os": [
|
|
22
|
-
"win32"
|
|
23
|
-
],
|
|
24
|
-
"engines": {
|
|
25
|
-
"node": "^22.19.0 || >=24.0.0"
|
|
26
|
-
},
|
|
27
|
-
"dependencies": {
|
|
28
|
-
"better-sqlite3-multiple-ciphers": "^13.0.3",
|
|
29
|
-
"ws": "^8.21.3"
|
|
30
|
-
},
|
|
31
|
-
"keywords": [
|
|
32
|
-
"cortico-world",
|
|
33
|
-
"cortico",
|
|
34
|
-
"wechat",
|
|
35
|
-
"weixin",
|
|
36
|
-
"notifier"
|
|
37
|
-
],
|
|
38
|
-
"cortico": {
|
|
39
|
-
"displayName": "微信消息提示",
|
|
40
|
-
"kind": "world",
|
|
41
|
-
"api": 6,
|
|
42
|
-
"describe": "describe.json",
|
|
43
|
-
"tags": [
|
|
44
|
-
"im",
|
|
45
|
-
"external-setup",
|
|
46
|
-
"experimental",
|
|
47
|
-
"sensitive-data"
|
|
48
|
-
]
|
|
49
|
-
},
|
|
50
|
-
"scripts": {
|
|
51
|
-
"test": "node --test test/format.test.mjs test/world.test.mjs"
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "cortico-world-wechat",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"author": "静音胡闹 <2310468741@qq.com>",
|
|
5
|
+
"description": "把 Windows 微信新消息播报给桌宠:扩展自己解密并轮询微信本地库,把新消息推进桌宠的事件流,由桌宠决定怎么汇报。",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"tools",
|
|
16
|
+
"test",
|
|
17
|
+
"describe.json",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"os": [
|
|
22
|
+
"win32"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"better-sqlite3-multiple-ciphers": "^13.0.3",
|
|
29
|
+
"ws": "^8.21.3"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"cortico-world",
|
|
33
|
+
"cortico",
|
|
34
|
+
"wechat",
|
|
35
|
+
"weixin",
|
|
36
|
+
"notifier"
|
|
37
|
+
],
|
|
38
|
+
"cortico": {
|
|
39
|
+
"displayName": "微信消息提示",
|
|
40
|
+
"kind": "world",
|
|
41
|
+
"api": 6,
|
|
42
|
+
"describe": "describe.json",
|
|
43
|
+
"tags": [
|
|
44
|
+
"im",
|
|
45
|
+
"external-setup",
|
|
46
|
+
"experimental",
|
|
47
|
+
"sensitive-data"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"test": "node --test test/format.test.mjs test/world.test.mjs"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/format.js
CHANGED
|
@@ -1,50 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 微信事件帧 →
|
|
2
|
+
* 微信事件帧 → 给桌宠看的一句"转达请求"。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 为什么不是"通知":早期版本写「XX 发来消息:<原文>。跟胡闹说一声。」,
|
|
5
|
+
* 结果她**把原文当成对她说的**,直接接着聊(实测:原文"你在干嘛呢?",
|
|
6
|
+
* 她答"在呢在呢!")。原因是问句本身在邀请回答,而"转达"只是个附加从句。
|
|
7
|
+
* 所以现在把**动作要求放前面并写明"别自己回答"**。
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
+
* 分档(按用户要求):
|
|
10
|
+
* - 单条、内容短 → **照原文转达**,保证不失真;
|
|
11
|
+
* - 内容长 → **只转述要点**,不搬运长篇(省上下文,也避免她复述长文时跑偏)。
|
|
12
|
+
* 真正的"消息多了"由 Python 侧之外的监听层限流负责(同一会话冷却/全局间隔),
|
|
13
|
+
* 所以这里只需要处理"单条的长度"这一个维度。
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
|
-
/**
|
|
12
|
-
const
|
|
16
|
+
/** 引述原文的上限:超过就改为"只转述要点" */
|
|
17
|
+
export const DEFAULT_RELAY_QUOTE_MAX = 40;
|
|
18
|
+
/** 转达请求本身的总长上限,避免一帧把气泡撑爆 */
|
|
19
|
+
const TEXT_MAX = 200;
|
|
13
20
|
|
|
14
|
-
/**
|
|
15
|
-
function clip(text, max
|
|
21
|
+
/** 折叠空白并截断 */
|
|
22
|
+
function clip(text, max) {
|
|
16
23
|
const s = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
17
|
-
if (s.length <= max) return s;
|
|
24
|
+
if (!max || s.length <= max) return s;
|
|
18
25
|
return `${s.slice(0, max)}…`;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
/**
|
|
22
|
-
* 事件帧 →
|
|
29
|
+
* 事件帧 → 转达请求文本。
|
|
23
30
|
*
|
|
24
|
-
*
|
|
25
|
-
* { t:'notice',
|
|
26
|
-
*
|
|
31
|
+
* 帧形状(监听层产出,字段来自微信库实测):
|
|
32
|
+
* { t:'notice', kind:'message', chat, chat_username, sender, is_group,
|
|
33
|
+
* preview, ts, local_id, shard, relay_quote_max? }
|
|
27
34
|
*
|
|
28
35
|
* @param {Record<string, unknown>} frame
|
|
36
|
+
* @param {{ relayQuoteMax?: number }} [options]
|
|
29
37
|
* @returns {string | null} null = 这一帧不需要她知道
|
|
30
38
|
*/
|
|
31
|
-
export function frameToText(frame) {
|
|
39
|
+
export function frameToText(frame, options = {}) {
|
|
32
40
|
if (!frame || typeof frame !== 'object') return null;
|
|
33
|
-
if (frame.t !== 'notice') return null;
|
|
41
|
+
if (frame.t !== 'notice') return null;
|
|
34
42
|
if (frame.kind && frame.kind !== 'message') return null;
|
|
35
43
|
|
|
36
44
|
const chat = clip(frame.chat, 40) || '某个会话';
|
|
37
45
|
const sender = clip(frame.sender, 24) || '某人';
|
|
38
46
|
const preview = clip(frame.preview, 100);
|
|
47
|
+
const quoteMax = Number(options.relayQuoteMax ?? frame.relay_quote_max ?? DEFAULT_RELAY_QUOTE_MAX);
|
|
39
48
|
|
|
40
|
-
//
|
|
49
|
+
// 自己发的消息不催办
|
|
41
50
|
if (sender === '我') {
|
|
42
|
-
return `【微信】你在「${chat}
|
|
51
|
+
return `【微信】你在「${chat}」发了一条消息。不用转达。`;
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
const where = frame.is_group === true ? `群「${chat}」里的 ${sender}` : sender;
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
|
|
56
|
+
// 短内容:照原文转达(带"别回答"约束)
|
|
57
|
+
if (preview && preview.length <= quoteMax) {
|
|
58
|
+
return `【微信】${where}发来消息:「${preview}」。转达给胡闹,别自己回答。`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 长内容 / 无正文:只转述要点
|
|
62
|
+
const what = preview ? '内容较长' : '没有文字正文(可能是图片或文件)';
|
|
63
|
+
return `【微信】${where}发来消息(${what})。用一句话转述要点给胡闹,别照抄、别自己回答。`;
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
export { clip };
|
package/src/world.js
CHANGED
|
@@ -43,6 +43,8 @@ export function wechatDefaults() {
|
|
|
43
43
|
cacheDir: '',
|
|
44
44
|
/** 轮询间隔(秒)。 */
|
|
45
45
|
pollIntervalSec: 1,
|
|
46
|
+
/** 转达口径:正文短于此长度就照原文转达,超过只转述要点(避免她复述长文跑偏)。 */
|
|
47
|
+
relayQuoteMax: 40,
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -92,9 +94,11 @@ export class WechatWorld {
|
|
|
92
94
|
return {
|
|
93
95
|
wechat_notifier_url: String(this.cfg.url),
|
|
94
96
|
wechat_notifier_duty:
|
|
95
|
-
'
|
|
96
|
-
+ '
|
|
97
|
-
+ '
|
|
97
|
+
'微信消息提示是给你看的:有人在微信里给胡闹发了消息。'
|
|
98
|
+
+ '你的任务是**转达**——用 pet_say 把"谁、在哪个会话、说了什么"简短告诉胡闹,'
|
|
99
|
+
+ '短消息照原话说,长消息只讲要点。'
|
|
100
|
+
+ '注意:那条消息是说给胡闹的,不是问你的,别自己回答、别接着聊天。'
|
|
101
|
+
+ '自己发的消息不用转达。',
|
|
98
102
|
};
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -192,7 +196,7 @@ export class WechatWorld {
|
|
|
192
196
|
this.log(`配置关闭群消息,跳过:${event.chat ?? ''}`);
|
|
193
197
|
return;
|
|
194
198
|
}
|
|
195
|
-
const text = frameToText(event);
|
|
199
|
+
const text = frameToText(event, { relayQuoteMax: this.cfg.relayQuoteMax ?? 40 });
|
|
196
200
|
if (!text) return;
|
|
197
201
|
this.log(`收到 ${event.chat ?? '?'} 的消息 → 推给她:${text.slice(0, 60)}`);
|
|
198
202
|
void this.push(text);
|
package/test/format.test.mjs
CHANGED
|
@@ -1,42 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* format.js 单测:把"她会听到什么"钉死。
|
|
3
|
-
* 运行:node test/format.test.mjs
|
|
3
|
+
* 运行:node test/format.test.mjs(受限沙箱里不要用 node --test)
|
|
4
4
|
*/
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
6
|
import test from 'node:test';
|
|
7
7
|
|
|
8
|
-
import { frameToText } from '../src/format.js';
|
|
8
|
+
import { DEFAULT_RELAY_QUOTE_MAX, frameToText } from '../src/format.js';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
assert.
|
|
10
|
+
const base = { t: 'notice', kind: 'message', is_group: false };
|
|
11
|
+
|
|
12
|
+
test('单聊短消息:照原文转达,并明确"别自己回答"', () => {
|
|
13
|
+
const text = frameToText({ ...base, chat: '张三', sender: '张三', preview: '下午三点开会' });
|
|
14
|
+
assert.equal(text, '【微信】张三发来消息:「下午三点开会」。转达给胡闹,别自己回答。');
|
|
15
|
+
assert.ok(text.includes('别自己回答'), '必须带"别回答"约束——否则她会把问句当成对她说的');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('群聊短消息:带群名与发言人', () => {
|
|
19
|
+
const text = frameToText({ ...base, chat: '云南项目部', sender: '李四', is_group: true, preview: '明天上线' });
|
|
20
|
+
assert.equal(text, '【微信】群「云南项目部」里的 李四发来消息:「明天上线」。转达给胡闹,别自己回答。');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('问句短消息:仍只是转达,不给她"回答问题"的空间', () => {
|
|
24
|
+
// 这条是实测教训:原文"你在干嘛呢?"曾让她答"在呢在呢!"
|
|
25
|
+
const text = frameToText({ ...base, chat: '静音胡闹', sender: '静音胡闹', preview: '你在干嘛呢?' });
|
|
26
|
+
assert.ok(text.endsWith('转达给胡闹,别自己回答。'));
|
|
27
|
+
assert.ok(!text.includes('告诉'), '不应出现"回答我"这类措辞');
|
|
16
28
|
});
|
|
17
29
|
|
|
18
|
-
test('
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
assert.
|
|
30
|
+
test('长消息:只转述要点,不搬运原文', () => {
|
|
31
|
+
const long = '这是一段很长的正文,'.repeat(8); // 80 字 > 默认阈值
|
|
32
|
+
const text = frameToText({ ...base, chat: '张三', sender: '张三', preview: long });
|
|
33
|
+
assert.ok(text.includes('内容较长'), `应提示内容较长:${text}`);
|
|
34
|
+
assert.ok(text.includes('用一句话转述要点'), '应要求只转述要点');
|
|
35
|
+
assert.ok(!text.includes(long.slice(0, 20)), '不应照抄长文');
|
|
36
|
+
assert.ok(text.length < 120, '转达请求本身要短');
|
|
24
37
|
});
|
|
25
38
|
|
|
26
|
-
test('
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
assert.
|
|
32
|
-
assert.ok(!text.includes('胡闹说一声'), '自己发的消息不该催她汇报');
|
|
39
|
+
test('阈值可配:调大后短消息也走"照原文"', () => {
|
|
40
|
+
const preview = '一二三四五六七八九十';
|
|
41
|
+
const quoted = frameToText({ ...base, chat: '张三', sender: '张三', preview }, { relayQuoteMax: 100 });
|
|
42
|
+
assert.ok(quoted.includes(preview), '阈值调大后应照原文转达');
|
|
43
|
+
const terse = frameToText({ ...base, chat: '张三', sender: '张三', preview }, { relayQuoteMax: 5 });
|
|
44
|
+
assert.ok(terse.includes('内容较长'), '阈值调小后应只转述要点');
|
|
33
45
|
});
|
|
34
46
|
|
|
35
|
-
test('
|
|
36
|
-
const text = frameToText({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
test('无正文(图片/文件):给转述口径,不出现空串', () => {
|
|
48
|
+
const text = frameToText({ ...base, chat: '张三', sender: '张三', preview: '' });
|
|
49
|
+
assert.ok(text.includes('没有文字正文'), `应说明无正文:${text}`);
|
|
50
|
+
assert.ok(text.includes('转述要点'));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('自己发的消息:不催办', () => {
|
|
54
|
+
const text = frameToText({ ...base, chat: '文件传输助手', sender: '我', preview: '测试' });
|
|
55
|
+
assert.ok(text.includes('不用转达'), '自己发的不该让她转达');
|
|
56
|
+
assert.ok(!text.includes('转达给胡闹'));
|
|
40
57
|
});
|
|
41
58
|
|
|
42
59
|
test('控制帧不产生正文', () => {
|
|
@@ -45,11 +62,12 @@ test('控制帧不产生正文', () => {
|
|
|
45
62
|
assert.equal(frameToText({ t: 'notice', kind: 'other' }), null);
|
|
46
63
|
});
|
|
47
64
|
|
|
48
|
-
test('
|
|
49
|
-
const text = frameToText({
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
assert.
|
|
65
|
+
test('超长内容被截断,请求整体不会撑爆气泡', () => {
|
|
66
|
+
const text = frameToText({ ...base, chat: '张三', sender: '张三', preview: '长'.repeat(500) });
|
|
67
|
+
assert.ok(text.length < 220, `应受总长限制,实际 ${text.length}`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('默认阈值是 40 字', () => {
|
|
71
|
+
assert.equal(DEFAULT_RELAY_QUOTE_MAX, 40);
|
|
55
72
|
});
|
|
73
|
+
|
package/test/world.test.mjs
CHANGED
|
@@ -97,12 +97,14 @@ test('默认配置符合预期(仅回环、默认关闭)', () => {
|
|
|
97
97
|
assert.equal(defaults.notifyGroup, true);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
test('envPromptVars 必须含"
|
|
100
|
+
test('envPromptVars 必须含"转达、别自己回答"的职责说明', () => {
|
|
101
101
|
const world = new WechatWorld(WECHAT_ID, {}, loadWs);
|
|
102
102
|
const vars = world.envPromptVars();
|
|
103
103
|
assert.ok(vars.wechat_notifier_url, '应注入地址变量');
|
|
104
|
-
assert.ok(vars.wechat_notifier_duty.includes('pet_say'), '
|
|
105
|
-
|
|
104
|
+
assert.ok(vars.wechat_notifier_duty.includes('pet_say'), '要指明用 pet_say 汇报');
|
|
105
|
+
// 这条是实测教训:只写"跟胡闹说一声"时,她会把消息原文当成对她说的、直接接着聊
|
|
106
|
+
assert.ok(vars.wechat_notifier_duty.includes('别自己回答'), '必须写明别自己回答');
|
|
107
|
+
assert.ok(vars.wechat_notifier_duty.includes('转达'), '应说明任务是转达');
|
|
106
108
|
});
|
|
107
109
|
|
|
108
110
|
test('客户端连上后收到 hello', async () => {
|
|
@@ -118,19 +120,24 @@ test('客户端连上后收到 hello', async () => {
|
|
|
118
120
|
|
|
119
121
|
test('收到微信消息 -> 翻译并 pushEvent(带 trigger:flush 与 worldId.notice 类型)', async () => {
|
|
120
122
|
const { world, pushes, port } = await startWorld();
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
123
|
+
try {
|
|
124
|
+
const socket = await connect(port);
|
|
125
|
+
socket.send(JSON.stringify({
|
|
126
|
+
t: 'notice', kind: 'message', chat: '张三', sender: '张三',
|
|
127
|
+
is_group: false, preview: '下午三点开会', ts: 1792137092000,
|
|
128
|
+
}));
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
130
|
+
assert.equal(pushes.length, 1);
|
|
131
|
+
assert.equal(pushes[0].event.type, `${WECHAT_ID}.notice`);
|
|
132
|
+
assert.equal(pushes[0].event.source, WECHAT_ID);
|
|
133
|
+
// 转达口径:带原文引号 + "别自己回答"
|
|
134
|
+
assert.match(pushes[0].event.text, /张三发来消息:「下午三点开会」/);
|
|
135
|
+
assert.match(pushes[0].event.text, /转达给胡闹,别自己回答/);
|
|
136
|
+
assert.equal(pushes[0].opts.trigger, 'flush');
|
|
137
|
+
socket.close();
|
|
138
|
+
} finally {
|
|
139
|
+
await world.stop();
|
|
140
|
+
}
|
|
134
141
|
});
|
|
135
142
|
|
|
136
143
|
test('自己发的消息按配置不推送', async () => {
|