inkdrift 0.1.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.
- package/.env.example +23 -0
- package/README.md +119 -0
- package/dist/index-once.d.ts +8 -0
- package/dist/index-once.d.ts.map +1 -0
- package/dist/index-once.js +45 -0
- package/dist/index-once.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/bot/index.d.ts +11 -0
- package/dist/modules/bot/index.d.ts.map +1 -0
- package/dist/modules/bot/index.js +60 -0
- package/dist/modules/bot/index.js.map +1 -0
- package/dist/modules/bot/test.d.ts +13 -0
- package/dist/modules/bot/test.d.ts.map +1 -0
- package/dist/modules/bot/test.js +37 -0
- package/dist/modules/bot/test.js.map +1 -0
- package/dist/modules/distiller/index.d.ts +24 -0
- package/dist/modules/distiller/index.d.ts.map +1 -0
- package/dist/modules/distiller/index.js +124 -0
- package/dist/modules/distiller/index.js.map +1 -0
- package/dist/modules/distiller/test.d.ts +10 -0
- package/dist/modules/distiller/test.d.ts.map +1 -0
- package/dist/modules/distiller/test.js +92 -0
- package/dist/modules/distiller/test.js.map +1 -0
- package/dist/modules/extractor/index.d.ts +52 -0
- package/dist/modules/extractor/index.d.ts.map +1 -0
- package/dist/modules/extractor/index.js +114 -0
- package/dist/modules/extractor/index.js.map +1 -0
- package/dist/modules/extractor/test.d.ts +10 -0
- package/dist/modules/extractor/test.d.ts.map +1 -0
- package/dist/modules/extractor/test.js +80 -0
- package/dist/modules/extractor/test.js.map +1 -0
- package/dist/modules/fetcher/index.d.ts +2 -0
- package/dist/modules/fetcher/index.d.ts.map +1 -0
- package/dist/modules/fetcher/index.js +3 -0
- package/dist/modules/fetcher/index.js.map +1 -0
- package/dist/modules/fetcher/test.d.ts +2 -0
- package/dist/modules/fetcher/test.d.ts.map +1 -0
- package/dist/modules/fetcher/test.js +3 -0
- package/dist/modules/fetcher/test.js.map +1 -0
- package/dist/modules/flowus-source/index.d.ts +55 -0
- package/dist/modules/flowus-source/index.d.ts.map +1 -0
- package/dist/modules/flowus-source/index.js +195 -0
- package/dist/modules/flowus-source/index.js.map +1 -0
- package/dist/modules/flowus-source/test.d.ts +14 -0
- package/dist/modules/flowus-source/test.d.ts.map +1 -0
- package/dist/modules/flowus-source/test.js +63 -0
- package/dist/modules/flowus-source/test.js.map +1 -0
- package/dist/modules/flowus-source/types.d.ts +93 -0
- package/dist/modules/flowus-source/types.d.ts.map +1 -0
- package/dist/modules/flowus-source/types.js +5 -0
- package/dist/modules/flowus-source/types.js.map +1 -0
- package/dist/modules/memorizer/index.d.ts +29 -0
- package/dist/modules/memorizer/index.d.ts.map +1 -0
- package/dist/modules/memorizer/index.js +72 -0
- package/dist/modules/memorizer/index.js.map +1 -0
- package/dist/modules/memorizer/test.d.ts +10 -0
- package/dist/modules/memorizer/test.d.ts.map +1 -0
- package/dist/modules/memorizer/test.js +69 -0
- package/dist/modules/memorizer/test.js.map +1 -0
- package/dist/modules/queue/index.d.ts +45 -0
- package/dist/modules/queue/index.d.ts.map +1 -0
- package/dist/modules/queue/index.js +161 -0
- package/dist/modules/queue/index.js.map +1 -0
- package/dist/modules/queue/test.d.ts +2 -0
- package/dist/modules/queue/test.d.ts.map +1 -0
- package/dist/modules/queue/test.js +3 -0
- package/dist/modules/queue/test.js.map +1 -0
- package/dist/modules/webhook/index.d.ts +8 -0
- package/dist/modules/webhook/index.d.ts.map +1 -0
- package/dist/modules/webhook/index.js +8 -0
- package/dist/modules/webhook/index.js.map +1 -0
- package/dist/modules/webhook/test.d.ts +12 -0
- package/dist/modules/webhook/test.d.ts.map +1 -0
- package/dist/modules/webhook/test.js +14 -0
- package/dist/modules/webhook/test.js.map +1 -0
- package/dist/modules/wechat-client/index.d.ts +8 -0
- package/dist/modules/wechat-client/index.d.ts.map +1 -0
- package/dist/modules/wechat-client/index.js +8 -0
- package/dist/modules/wechat-client/index.js.map +1 -0
- package/dist/modules/wechat-client/test.d.ts +12 -0
- package/dist/modules/wechat-client/test.d.ts.map +1 -0
- package/dist/modules/wechat-client/test.js +14 -0
- package/dist/modules/wechat-client/test.js.map +1 -0
- package/dist/modules/wechat-fetcher/index.d.ts +18 -0
- package/dist/modules/wechat-fetcher/index.d.ts.map +1 -0
- package/dist/modules/wechat-fetcher/index.js +75 -0
- package/dist/modules/wechat-fetcher/index.js.map +1 -0
- package/dist/scripts/reset-one.d.ts +6 -0
- package/dist/scripts/reset-one.d.ts.map +1 -0
- package/dist/scripts/reset-one.js +40 -0
- package/dist/scripts/reset-one.js.map +1 -0
- package/dist/utils/config.d.ts +24 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +35 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +3 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +3 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +61 -0
package/.env.example
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# FlowUs API (第一期数据源)
|
|
2
|
+
FLOWUS_BASE_URL=https://api.flowus.cn
|
|
3
|
+
FLOWUS_TOKEN=your_flowus_api_token
|
|
4
|
+
FLOWUS_DATABASE_ID=your_database_id
|
|
5
|
+
|
|
6
|
+
# GLM API
|
|
7
|
+
GLM_BASE_URL=https://aiping.cn/api/v1
|
|
8
|
+
GLM_API_KEY=your-key-here
|
|
9
|
+
GLM_MODEL=GLM-4.7
|
|
10
|
+
|
|
11
|
+
# 轮询(常驻模式)
|
|
12
|
+
POLL_INTERVAL_MS=10000
|
|
13
|
+
|
|
14
|
+
# Web
|
|
15
|
+
WEB_PORT=3000
|
|
16
|
+
|
|
17
|
+
# Retry
|
|
18
|
+
RETRY_MAX=3
|
|
19
|
+
RETRY_INTERVAL_MS=60000
|
|
20
|
+
|
|
21
|
+
# Log
|
|
22
|
+
LOG_LEVEL=info
|
|
23
|
+
LOG_DIR=./logs
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# inkdrift
|
|
2
|
+
|
|
3
|
+
微信文章知识蒸馏机器人。从 FlowUs 剪藏的文章中自动提取知识点,经 LLM 提炼后存入 nmem 记忆库。
|
|
4
|
+
|
|
5
|
+
## 架构
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
FlowUs 剪藏 → flowus-source → extractor → distiller → memorizer → nmem
|
|
9
|
+
(API 读取) (清洗) (LLM提炼) (CLI写入)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 快速开始
|
|
13
|
+
|
|
14
|
+
### 环境要求
|
|
15
|
+
|
|
16
|
+
- Node.js 20+
|
|
17
|
+
- [nmem](https://github.com/nmem/nmem) CLI(运行 `nmem serve` 启动本地服务)
|
|
18
|
+
|
|
19
|
+
### 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone <repo-url> && cd inkdrift
|
|
23
|
+
cp .env.example .env
|
|
24
|
+
# 编辑 .env 填写配置(见下方配置说明)
|
|
25
|
+
npm install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 配置
|
|
29
|
+
|
|
30
|
+
在 `.env` 文件中配置以下环境变量:
|
|
31
|
+
|
|
32
|
+
| 变量 | 必填 | 说明 |
|
|
33
|
+
|------|------|------|
|
|
34
|
+
| `FLOWUS_TOKEN` | 是 | FlowUs API Token |
|
|
35
|
+
| `FLOWUS_DATABASE_ID` | 是 | FlowUs 多维表 ID |
|
|
36
|
+
| `GLM_API_KEY` | 是 | GLM API Key(OpenAI 兼容格式) |
|
|
37
|
+
| `GLM_BASE_URL` | 否 | LLM API 地址,默认 `https://aiping.cn/api/v1` |
|
|
38
|
+
| `GLM_MODEL` | 否 | 模型名称,默认 `GLM-4.7` |
|
|
39
|
+
| `FLOWUS_BASE_URL` | 否 | FlowUs API 地址,默认 `https://api.flowus.cn` |
|
|
40
|
+
| `WEB_PORT` | 否 | Web 状态页端口,默认 `3000` |
|
|
41
|
+
| `RETRY_MAX` | 否 | 最大重试次数,默认 `3` |
|
|
42
|
+
| `RETRY_INTERVAL_MS` | 否 | 重试间隔(毫秒),默认 `60000` |
|
|
43
|
+
| `LOG_LEVEL` | 否 | 日志级别,默认 `info` |
|
|
44
|
+
| `LOG_DIR` | 否 | 日志目录,默认 `./logs` |
|
|
45
|
+
| `POLL_INTERVAL_MS` | 否 | 轮询间隔(毫秒),仅常驻模式,默认 `10000` |
|
|
46
|
+
|
|
47
|
+
## 使用
|
|
48
|
+
|
|
49
|
+
### npx 运行(无需 clone)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# 1. 在工作目录创建 .env 配置文件
|
|
53
|
+
cp .env.example .env
|
|
54
|
+
# 编辑 .env 填写 FLOWUS_TOKEN、FLOWUS_DATABASE_ID、GLM_API_KEY
|
|
55
|
+
|
|
56
|
+
# 2. 启动 nmem 服务
|
|
57
|
+
nmem serve &
|
|
58
|
+
|
|
59
|
+
# 3. 运行
|
|
60
|
+
npx inkdrift
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 本地开发
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm run dev # 常驻轮询(默认)
|
|
67
|
+
npm run dev:once # 单次执行
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 自定义间隔(30 秒)
|
|
72
|
+
POLL_INTERVAL_MS=30000 npm run dev
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`Ctrl+C` 优雅退出。
|
|
76
|
+
|
|
77
|
+
## 项目结构
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
src/
|
|
81
|
+
├── index.ts # 常驻轮询入口(默认)
|
|
82
|
+
├── index-once.ts # 单次执行入口
|
|
83
|
+
├── utils/
|
|
84
|
+
│ ├── config.ts # 集中配置(读取 .env)
|
|
85
|
+
│ └── logger.ts # 日志工具
|
|
86
|
+
├── modules/
|
|
87
|
+
│ ├── flowus-source/ # FlowUs API 客户端(读取文章、更新状态)
|
|
88
|
+
│ ├── extractor/ # Markdown 清洗 + 元数据提取
|
|
89
|
+
│ ├── distiller/ # LLM 知识提炼(GLM-4.7)
|
|
90
|
+
│ ├── memorizer/ # nmem CLI 写入 + JSONL 日志
|
|
91
|
+
│ ├── queue/ # 串行处理队列(串联所有模块)
|
|
92
|
+
│ ├── bot/ # [二期] wechaty 方案(已弃用)
|
|
93
|
+
│ ├── fetcher/ # [一期跳过] flowus-source 已含内容提取
|
|
94
|
+
│ ├── webhook/ # [预留]
|
|
95
|
+
│ └── wechat-client/ # [预留]
|
|
96
|
+
├── scripts/
|
|
97
|
+
│ └── reset-one.ts # 重置一篇文章为"未处理"(测试用)
|
|
98
|
+
└── web/
|
|
99
|
+
└── server.ts # Web 状态页
|
|
100
|
+
docs/
|
|
101
|
+
├── architecture.md # 整体架构
|
|
102
|
+
├── adr/ # 架构决策记录
|
|
103
|
+
└── modules/ # 各模块设计文档
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 两阶段规划
|
|
107
|
+
|
|
108
|
+
### Phase 1(当前)
|
|
109
|
+
|
|
110
|
+
- FlowUs 作为文章数据源
|
|
111
|
+
- 用户在微信中手动剪藏文章到 FlowUs 多维表
|
|
112
|
+
- 机器人通过 FlowUs API 读取、处理、提炼知识点
|
|
113
|
+
- 零封号风险
|
|
114
|
+
|
|
115
|
+
### Phase 2(规划中)
|
|
116
|
+
|
|
117
|
+
- 接入 WeChatPadPro(iPad 协议)实现全自动监听
|
|
118
|
+
- 自动接收微信文章、处理、存储,无需手动剪藏
|
|
119
|
+
- 详见 `docs/adr/003-wechatpadpro-phase2.md`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-once.d.ts","sourceRoot":"","sources":["../src/index-once.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inkdrift single-run entry point
|
|
3
|
+
*
|
|
4
|
+
* Processes all pending articles once, then exits.
|
|
5
|
+
* Usage: npx tsx src/index-once.ts
|
|
6
|
+
*/
|
|
7
|
+
import './utils/config.js';
|
|
8
|
+
import { config } from './utils/config.js';
|
|
9
|
+
import { SerialQueue } from './modules/queue/index.js';
|
|
10
|
+
// Validate required config
|
|
11
|
+
if (!config.flowus.token || !config.flowus.databaseId) {
|
|
12
|
+
console.error('Missing FLOWUS_TOKEN or FLOWUS_DATABASE_ID in .env');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
if (!config.glm.apiKey) {
|
|
16
|
+
console.error('Missing GLM_API_KEY in .env');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
console.log('=== inkdrift pipeline ===\n');
|
|
20
|
+
const queue = new SerialQueue({
|
|
21
|
+
flowus: config.flowus,
|
|
22
|
+
distill: {
|
|
23
|
+
baseUrl: config.glm.baseUrl,
|
|
24
|
+
apiKey: config.glm.apiKey,
|
|
25
|
+
model: config.glm.model,
|
|
26
|
+
},
|
|
27
|
+
logDir: config.log.dir,
|
|
28
|
+
});
|
|
29
|
+
const results = await queue.processAll();
|
|
30
|
+
// Print summary
|
|
31
|
+
console.log('=== Summary ===');
|
|
32
|
+
console.log(` Total: ${results.length}`);
|
|
33
|
+
console.log(` Success: ${results.filter((r) => r.success).length}`);
|
|
34
|
+
console.log(` Failed: ${results.filter((r) => !r.success).length}`);
|
|
35
|
+
const totalPoints = results.reduce((sum, r) => sum + r.pointsCount, 0);
|
|
36
|
+
const totalSaved = results.reduce((sum, r) => sum + r.savedCount, 0);
|
|
37
|
+
console.log(` Points: ${totalPoints} extracted, ${totalSaved} saved`);
|
|
38
|
+
if (results.some((r) => !r.success)) {
|
|
39
|
+
console.log('\n Failed articles:');
|
|
40
|
+
for (const r of results.filter((r) => !r.success)) {
|
|
41
|
+
console.log(` - ${r.task.title}: ${r.error}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
console.log('\n=== Done ===');
|
|
45
|
+
//# sourceMappingURL=index-once.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-once.js","sourceRoot":"","sources":["../src/index-once.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,mBAAmB,CAAC;AAC3B,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,2BAA2B;AAC3B,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IACtD,OAAO,CAAC,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACpE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;IACvB,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;IAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;AAE3C,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM;IACrB,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO;QAC3B,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM;QACzB,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK;KACxB;IACD,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG;CACvB,CAAC,CAAC;AAEH,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,UAAU,EAAE,CAAC;AAEzC,gBAAgB;AAChB,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;AAC/B,OAAO,CAAC,GAAG,CAAC,cAAc,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AAC5C,OAAO,CAAC,GAAG,CAAC,cAAc,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AACrE,OAAO,CAAC,GAAG,CAAC,cAAc,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AAEtE,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AACvE,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;AACrE,OAAO,CAAC,GAAG,CAAC,cAAc,WAAW,eAAe,UAAU,QAAQ,CAAC,CAAC;AAExE,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACpC,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IACpC,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;QAClD,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IACnD,CAAC;AACH,CAAC;AAED,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,OAAO,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* inkdrift entry point
|
|
4
|
+
*
|
|
5
|
+
* Runs the pipeline in a loop with configurable interval.
|
|
6
|
+
* Usage: npx tsx src/index.ts
|
|
7
|
+
*/
|
|
8
|
+
import './utils/config.js';
|
|
9
|
+
import { config } from './utils/config.js';
|
|
10
|
+
import { SerialQueue } from './modules/queue/index.js';
|
|
11
|
+
// Validate required config
|
|
12
|
+
if (!config.flowus.token || !config.flowus.databaseId) {
|
|
13
|
+
console.error('Missing FLOWUS_TOKEN or FLOWUS_DATABASE_ID in .env');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
if (!config.glm.apiKey) {
|
|
17
|
+
console.error('Missing GLM_API_KEY in .env');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const intervalMs = parseInt(process.env.POLL_INTERVAL_MS || '10000', 10);
|
|
21
|
+
const queue = new SerialQueue({
|
|
22
|
+
flowus: config.flowus,
|
|
23
|
+
distill: {
|
|
24
|
+
baseUrl: config.glm.baseUrl,
|
|
25
|
+
apiKey: config.glm.apiKey,
|
|
26
|
+
model: config.glm.model,
|
|
27
|
+
},
|
|
28
|
+
logDir: config.log.dir,
|
|
29
|
+
});
|
|
30
|
+
let running = true;
|
|
31
|
+
// Keep process alive with a ref'd timer
|
|
32
|
+
const keepAlive = setInterval(() => { }, 60000);
|
|
33
|
+
// Graceful shutdown
|
|
34
|
+
process.on('SIGINT', () => {
|
|
35
|
+
console.log('\nShutting down...');
|
|
36
|
+
running = false;
|
|
37
|
+
clearInterval(keepAlive);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
});
|
|
40
|
+
process.on('SIGTERM', () => {
|
|
41
|
+
console.log('\nShutting down...');
|
|
42
|
+
running = false;
|
|
43
|
+
clearInterval(keepAlive);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
});
|
|
46
|
+
console.log(`inkdrift polling started, interval: ${intervalMs / 1000}s\n`);
|
|
47
|
+
async function runOnce() {
|
|
48
|
+
if (!running)
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
const results = await queue.processAll();
|
|
52
|
+
if (results.length === 0) {
|
|
53
|
+
console.log('[poll] No new articles.\n');
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const success = results.filter((r) => r.success).length;
|
|
57
|
+
const failed = results.filter((r) => !r.success).length;
|
|
58
|
+
const totalPoints = results.reduce((sum, r) => sum + r.pointsCount, 0);
|
|
59
|
+
const totalSaved = results.reduce((sum, r) => sum + r.savedCount, 0);
|
|
60
|
+
console.log(`[poll] Round done: ${success} ok, ${failed} fail, ${totalPoints} points, ${totalSaved} saved\n`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
65
|
+
console.error(`[poll] Error: ${msg}\n`);
|
|
66
|
+
}
|
|
67
|
+
if (running) {
|
|
68
|
+
setTimeout(runOnce, intervalMs);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await runOnce();
|
|
72
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Wechaty } from 'wechaty';
|
|
2
|
+
export interface BotCallbacks {
|
|
3
|
+
onQRCode?: (qrData: string) => void;
|
|
4
|
+
onLogin?: (userName: string) => void;
|
|
5
|
+
onLogout?: () => void;
|
|
6
|
+
onArticleUrl?: (url: string, fromUser: string) => void;
|
|
7
|
+
onMessage?: (text: string, fromUser: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function createBot(callbacks?: BotCallbacks): Wechaty;
|
|
10
|
+
export declare function startBot(callbacks?: BotCallbacks): Promise<Wechaty>;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/modules/bot/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAoB,MAAM,SAAS,CAAC;AAIzD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACtD;AAED,wBAAgB,SAAS,CAAC,SAAS,GAAE,YAAiB,WAqDrD;AAED,wBAAsB,QAAQ,CAAC,SAAS,GAAE,YAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,CAK7E"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { WechatyBuilder, ScanStatus } from 'wechaty';
|
|
2
|
+
import qrcode from 'qrcode-terminal';
|
|
3
|
+
const WECHAT_ARTICLE_REGEX = /https?:\/\/mp\.weixin\.qq\.com\/[^\s<>"']+/;
|
|
4
|
+
export function createBot(callbacks = {}) {
|
|
5
|
+
const bot = WechatyBuilder.build({
|
|
6
|
+
name: 'inkdrift',
|
|
7
|
+
puppet: process.env.WECHATY_PUPPET || 'wechaty-puppet-wechat4u',
|
|
8
|
+
});
|
|
9
|
+
bot.on('scan', (qrcodeValue, status) => {
|
|
10
|
+
if (status === ScanStatus.Waiting) {
|
|
11
|
+
console.log('\n[QR] Scan this QR code to login:\n');
|
|
12
|
+
qrcode.generate(qrcodeValue, { small: true });
|
|
13
|
+
callbacks.onQRCode?.(qrcodeValue);
|
|
14
|
+
}
|
|
15
|
+
else if (status === ScanStatus.Scanned) {
|
|
16
|
+
console.log('[QR] Scanned, waiting for confirmation on phone...');
|
|
17
|
+
}
|
|
18
|
+
else if (status === ScanStatus.Confirmed) {
|
|
19
|
+
console.log('[QR] Confirmed!');
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
bot.on('login', (user) => {
|
|
23
|
+
console.log(`[LOGIN] Bot logged in as: ${user.name()} (${user.id})`);
|
|
24
|
+
callbacks.onLogin?.(user.name());
|
|
25
|
+
});
|
|
26
|
+
bot.on('logout', (user) => {
|
|
27
|
+
console.log(`[LOGOUT] Bot logged out: ${user.name()}`);
|
|
28
|
+
callbacks.onLogout?.();
|
|
29
|
+
});
|
|
30
|
+
bot.on('message', async (msg) => {
|
|
31
|
+
// Skip own messages
|
|
32
|
+
if (msg.self())
|
|
33
|
+
return;
|
|
34
|
+
const text = msg.text();
|
|
35
|
+
const contact = msg.talker();
|
|
36
|
+
const room = msg.room();
|
|
37
|
+
const fromLabel = room
|
|
38
|
+
? `Room:${await room.topic()} / ${contact.name()}`
|
|
39
|
+
: contact.name();
|
|
40
|
+
// Try to extract WeChat article URL
|
|
41
|
+
const match = text.match(WECHAT_ARTICLE_REGEX);
|
|
42
|
+
if (match) {
|
|
43
|
+
const url = match[0];
|
|
44
|
+
console.log(`[MSG] Article URL from ${fromLabel}: ${url}`);
|
|
45
|
+
callbacks.onArticleUrl?.(url, contact.id);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log(`[MSG] ${fromLabel}: ${text.substring(0, 80)}`);
|
|
49
|
+
callbacks.onMessage?.(text, contact.id);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return bot;
|
|
53
|
+
}
|
|
54
|
+
export async function startBot(callbacks = {}) {
|
|
55
|
+
const bot = createBot(callbacks);
|
|
56
|
+
await bot.start();
|
|
57
|
+
console.log('[BOT] inkdrift bot started, waiting for login...');
|
|
58
|
+
return bot;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/modules/bot/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bot module standalone test
|
|
3
|
+
*
|
|
4
|
+
* Usage: npx tsx src/modules/bot/test.ts
|
|
5
|
+
*
|
|
6
|
+
* Validation:
|
|
7
|
+
* 1. QR code appears in terminal
|
|
8
|
+
* 2. Scan to login successfully
|
|
9
|
+
* 3. Send a message to the bot and see it logged
|
|
10
|
+
* 4. Send a WeChat article link and see it recognized
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/modules/bot/test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bot module standalone test
|
|
3
|
+
*
|
|
4
|
+
* Usage: npx tsx src/modules/bot/test.ts
|
|
5
|
+
*
|
|
6
|
+
* Validation:
|
|
7
|
+
* 1. QR code appears in terminal
|
|
8
|
+
* 2. Scan to login successfully
|
|
9
|
+
* 3. Send a message to the bot and see it logged
|
|
10
|
+
* 4. Send a WeChat article link and see it recognized
|
|
11
|
+
*/
|
|
12
|
+
import { startBot } from './index.js';
|
|
13
|
+
console.log('=== inkdrift bot module test ===');
|
|
14
|
+
console.log('Starting bot, please scan QR code to login...\n');
|
|
15
|
+
const bot = await startBot({
|
|
16
|
+
onLogin(userName) {
|
|
17
|
+
console.log(`\n>>> TEST PASSED: Login successful as ${userName}\n`);
|
|
18
|
+
},
|
|
19
|
+
onLogout() {
|
|
20
|
+
console.log('\n>>> Bot logged out\n');
|
|
21
|
+
},
|
|
22
|
+
onArticleUrl(url, fromUser) {
|
|
23
|
+
console.log(`\n>>> TEST PASSED: Article detected!`);
|
|
24
|
+
console.log(` URL: ${url}`);
|
|
25
|
+
console.log(` From: ${fromUser}\n`);
|
|
26
|
+
},
|
|
27
|
+
onMessage(text, fromUser) {
|
|
28
|
+
console.log(`\n>>> Message received (non-article): ${text.substring(0, 60)}\n`);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
// Handle graceful shutdown
|
|
32
|
+
process.on('SIGINT', async () => {
|
|
33
|
+
console.log('\n>>> Shutting down...');
|
|
34
|
+
await bot.stop();
|
|
35
|
+
process.exit(0);
|
|
36
|
+
});
|
|
37
|
+
//# sourceMappingURL=test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.js","sourceRoot":"","sources":["../../../src/modules/bot/test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;AAChD,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC;AAE/D,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC;IACzB,OAAO,CAAC,QAAQ;QACd,OAAO,CAAC,GAAG,CAAC,0CAA0C,QAAQ,IAAI,CAAC,CAAC;IACtE,CAAC;IACD,QAAQ;QACN,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;IACxC,CAAC;IACD,YAAY,CAAC,GAAG,EAAE,QAAQ;QACxB,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;QACpD,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,EAAE,CAAC,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,aAAa,QAAQ,IAAI,CAAC,CAAC;IACzC,CAAC;IACD,SAAS,CAAC,IAAI,EAAE,QAAQ;QACtB,OAAO,CAAC,GAAG,CAAC,yCAAyC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;IAClF,CAAC;CACF,CAAC,CAAC;AAEH,2BAA2B;AAC3B,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;IAC9B,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;IACtC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* distiller module
|
|
3
|
+
*
|
|
4
|
+
* Sends Markdown article to LLM, extracts structured knowledge points,
|
|
5
|
+
* validates output with zod, and retries on failure.
|
|
6
|
+
*/
|
|
7
|
+
export interface KnowledgePoint {
|
|
8
|
+
title: string;
|
|
9
|
+
content: string;
|
|
10
|
+
tags: string[];
|
|
11
|
+
importance: number;
|
|
12
|
+
}
|
|
13
|
+
export interface DistillConfig {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
apiKey: string;
|
|
16
|
+
model: string;
|
|
17
|
+
maxRetries?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Distill knowledge points from a Markdown article.
|
|
21
|
+
* Appends source URL to each knowledge point's content.
|
|
22
|
+
*/
|
|
23
|
+
export declare function distill(markdown: string, url: string, config: DistillConfig): Promise<KnowledgePoint[]>;
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/modules/distiller/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAyCD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID;;;GAGG;AACH,wBAAsB,OAAO,CAC3B,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,cAAc,EAAE,CAAC,CAkD3B"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* distiller module
|
|
3
|
+
*
|
|
4
|
+
* Sends Markdown article to LLM, extracts structured knowledge points,
|
|
5
|
+
* validates output with zod, and retries on failure.
|
|
6
|
+
*/
|
|
7
|
+
import OpenAI from 'openai';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
// ---- Schema ----
|
|
10
|
+
const KnowledgePointSchema = z.object({
|
|
11
|
+
title: z.string().min(1),
|
|
12
|
+
content: z.string().min(10),
|
|
13
|
+
tags: z.array(z.string()).min(2).max(5),
|
|
14
|
+
importance: z.number().min(0).max(1),
|
|
15
|
+
});
|
|
16
|
+
const KnowledgePointsSchema = z.array(KnowledgePointSchema);
|
|
17
|
+
// ---- Prompt ----
|
|
18
|
+
const SYSTEM_PROMPT = `你是一个知识提炼助手。用户会给你一篇文章的 Markdown 内容,你需要从中提取独立的知识点。
|
|
19
|
+
|
|
20
|
+
要求:
|
|
21
|
+
1. 每个知识点必须独立可理解,不依赖上下文
|
|
22
|
+
2. content 要完整、具体,包含足够的细节
|
|
23
|
+
3. tags 为 2-5 个相关标签
|
|
24
|
+
4. importance 表示该知识点的价值(0.0-1.0)
|
|
25
|
+
5. 输出严格的 JSON 对象,包含一个 "points" 数组
|
|
26
|
+
6. 如果文章内容无实质知识(如纯广告、空内容),points 为空数组 []
|
|
27
|
+
7. 每篇文章最多提取 5 个知识点,优先选择价值最高、最有独立参考意义的内容
|
|
28
|
+
8. 排除纯信息性内容(如项目地址、技术规格、适用人群介绍、获取方式),只保留有独立知识价值的洞见、方法论或核心概念
|
|
29
|
+
|
|
30
|
+
输出格式示例:
|
|
31
|
+
{
|
|
32
|
+
"points": [
|
|
33
|
+
{
|
|
34
|
+
"title": "知识点标题",
|
|
35
|
+
"content": "知识点的详细内容描述",
|
|
36
|
+
"tags": ["标签1", "标签2"],
|
|
37
|
+
"importance": 0.8
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}`;
|
|
41
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
42
|
+
/**
|
|
43
|
+
* Distill knowledge points from a Markdown article.
|
|
44
|
+
* Appends source URL to each knowledge point's content.
|
|
45
|
+
*/
|
|
46
|
+
export async function distill(markdown, url, config) {
|
|
47
|
+
const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
48
|
+
const client = new OpenAI({
|
|
49
|
+
baseURL: config.baseUrl,
|
|
50
|
+
apiKey: config.apiKey,
|
|
51
|
+
});
|
|
52
|
+
const userMessage = `文章内容:\n\n${markdown}`;
|
|
53
|
+
let lastError = null;
|
|
54
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
55
|
+
try {
|
|
56
|
+
const response = await client.chat.completions.create({
|
|
57
|
+
model: config.model,
|
|
58
|
+
messages: [
|
|
59
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
60
|
+
{ role: 'user', content: userMessage },
|
|
61
|
+
],
|
|
62
|
+
temperature: 0.3,
|
|
63
|
+
response_format: { type: 'json_object' },
|
|
64
|
+
});
|
|
65
|
+
const raw = response.choices[0]?.message?.content;
|
|
66
|
+
if (!raw)
|
|
67
|
+
throw new Error('LLM returned empty response');
|
|
68
|
+
const parsed = parseJsonResponse(raw);
|
|
69
|
+
// Handle {"points": [...]} wrapper from json_object mode
|
|
70
|
+
const array = Array.isArray(parsed) ? parsed : parsed['points'] ?? parsed;
|
|
71
|
+
const validated = KnowledgePointsSchema.parse(array);
|
|
72
|
+
// Append source URL to each content
|
|
73
|
+
return validated.map((kp) => ({
|
|
74
|
+
...kp,
|
|
75
|
+
content: kp.content + `\n\n来源:${url}`,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
80
|
+
console.warn(`[distiller] Attempt ${attempt + 1}/${maxRetries + 1} failed: ${lastError.message}`);
|
|
81
|
+
if (attempt < maxRetries) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw lastError ?? new Error('distill failed after retries');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Try to parse JSON from LLM response.
|
|
90
|
+
* Handles cases where JSON is wrapped in markdown code blocks or has extra text.
|
|
91
|
+
*/
|
|
92
|
+
function parseJsonResponse(raw) {
|
|
93
|
+
// Direct parse
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(raw);
|
|
96
|
+
}
|
|
97
|
+
catch { /* fall through */ }
|
|
98
|
+
// Extract from code block
|
|
99
|
+
const codeBlock = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/)?.[1];
|
|
100
|
+
if (codeBlock)
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(codeBlock);
|
|
103
|
+
}
|
|
104
|
+
catch { /* fall through */ }
|
|
105
|
+
// Find JSON array
|
|
106
|
+
const arrayStr = raw.match(/\[[\s\S]*\]/)?.[0];
|
|
107
|
+
if (arrayStr)
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(arrayStr);
|
|
110
|
+
}
|
|
111
|
+
catch { /* fall through */ }
|
|
112
|
+
// Find JSON object, unwrap if it has an array value
|
|
113
|
+
const objStr = raw.match(/\{[\s\S]*\}/)?.[0];
|
|
114
|
+
if (objStr) {
|
|
115
|
+
try {
|
|
116
|
+
const obj = JSON.parse(objStr);
|
|
117
|
+
const arrayVal = Object.values(obj).find((v) => Array.isArray(v));
|
|
118
|
+
return arrayVal ?? obj;
|
|
119
|
+
}
|
|
120
|
+
catch { /* fall through */ }
|
|
121
|
+
}
|
|
122
|
+
throw new Error('No JSON found in LLM response');
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/modules/distiller/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* distiller module standalone test
|
|
3
|
+
*
|
|
4
|
+
* Usage: npx tsx src/modules/distiller/test.ts
|
|
5
|
+
*
|
|
6
|
+
* Reads a real article from FlowUs, extracts content, sends to LLM,
|
|
7
|
+
* prints distilled knowledge points.
|
|
8
|
+
*/
|
|
9
|
+
import '../../utils/config.js';
|
|
10
|
+
//# sourceMappingURL=test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/modules/distiller/test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,uBAAuB,CAAC"}
|