@aiyiran/myclaw 1.1.129 → 1.1.131

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/index.js CHANGED
@@ -108,12 +108,14 @@ function runInstall() {
108
108
  // ============================================================================
109
109
 
110
110
  const OPENCLAW_VERSION = '2026.5.7';
111
+ const OPENCLAW_VERSION_BETA = '2026.6.9';
111
112
 
112
- function runReinstall() {
113
+ function runReinstall(version, isBeta) {
114
+ const targetVersion = version || OPENCLAW_VERSION;
113
115
  const bar = '────────────────────────────────────────';
114
116
  console.log('');
115
117
  console.log(bar);
116
- console.log(' 🔄 OpenClaw 重装工具 (锁定版本: ' + colors.green + OPENCLAW_VERSION + colors.nc + ')');
118
+ console.log(' 🔄 OpenClaw 重装工具 (锁定版本: ' + colors.green + targetVersion + colors.nc + ')');
117
119
  console.log(bar);
118
120
  console.log('');
119
121
 
@@ -157,9 +159,9 @@ function runReinstall() {
157
159
  console.log('');
158
160
 
159
161
  // 5. 重新安装特定版本
160
- console.log('[5/8] 📦 安装 openclaw@' + OPENCLAW_VERSION + '...');
162
+ console.log('[5/8] 📦 安装 openclaw@' + targetVersion + '...');
161
163
  try {
162
- execSync('npm install -g openclaw@' + OPENCLAW_VERSION, {
164
+ execSync('npm install -g openclaw@' + targetVersion, {
163
165
  stdio: 'inherit',
164
166
  env: { ...process.env, npm_config_progress: 'true' }
165
167
  });
@@ -200,6 +202,19 @@ function runReinstall() {
200
202
  }
201
203
  console.log('');
202
204
 
205
+ // 9. (--beta 专属) 注入 MiniMax 6.X 兼容配置
206
+ if (isBeta) {
207
+ console.log('[9/9] 🔧 inject-minimax --beta — 修复 6.X 兼容配置...');
208
+ try {
209
+ const minimax = require('./injects/inject-minimax');
210
+ minimax.run(['--beta']);
211
+ console.log(' ' + colors.green + '✓ MiniMax 6.X 配置已修复' + colors.nc);
212
+ } catch (err) {
213
+ console.log(' ' + colors.yellow + '⚠ inject-minimax 失败: ' + err.message + colors.nc);
214
+ }
215
+ console.log('');
216
+ }
217
+
203
218
  console.log(bar);
204
219
  console.log(colors.green + ' ✅ 重装完成!' + colors.nc);
205
220
  console.log(bar);
@@ -1779,7 +1794,8 @@ function runInteractiveMenu() {
1779
1794
  const INJECT_MENU = [
1780
1795
  { key: '1', cmd: 'inject-minimax', desc: '注入 MiniMax 模型配置' },
1781
1796
  { key: '2', cmd: 'inject-zai', desc: '注入智谱 GLM 模型配置' },
1782
- { key: '3', cmd: 'inject-image', desc: '注入图像生成模型配置 (vveai)' },
1797
+ { key: '3', cmd: 'inject-claude', desc: '注入 B.AI 多模型配置 (Claude/GPT/Gemini)' },
1798
+ { key: '4', cmd: 'inject-image', desc: '注入图像生成模型配置 (vveai)' },
1783
1799
  { key: '4', cmd: 'inject-search', desc: '注入 Tavily 搜索插件配置' },
1784
1800
  { key: '5', cmd: 'inject-token', desc: '设置 Gateway Token 为 aiyiran' },
1785
1801
  { key: '6', cmd: 'inject-workspaceAndSoul', desc: '替换默认 workspace 的 SOUL.md' },
@@ -1792,6 +1808,7 @@ function runInjectCommand(cmd, extraArgs) {
1792
1808
  const modules = {
1793
1809
  'inject-minimax': './inject-minimax',
1794
1810
  'inject-zai': './inject-zai',
1811
+ 'inject-claude': './inject-claude',
1795
1812
  'inject-image': './inject-image',
1796
1813
  'inject-search': './inject-search',
1797
1814
  'inject-token': './inject-token',
@@ -2518,7 +2535,8 @@ function showHelp() {
2518
2535
  console.log('命令:');
2519
2536
  console.log(' start 智能启动(图标 & 命令行通用入口)');
2520
2537
  console.log(' install,i 安装 OpenClaw 服务');
2521
- console.log(' longxia 重装 OpenClaw (清理缓存 + 锁定版本)');
2538
+ console.log(' longxia 重装 OpenClaw (锁定 5.X 稳定版 ' + OPENCLAW_VERSION + ')');
2539
+ console.log(' longxia --beta 重装 OpenClaw (锁定 6.X 测试版 ' + OPENCLAW_VERSION_BETA + ')');
2522
2540
  console.log(' uninstall 卸载 MyClaw (恢复 npm 官方源)');
2523
2541
  console.log(' status 获取控制台网址');
2524
2542
  console.log(' update 自动升级 MyClaw 到最新版本');
@@ -2540,6 +2558,7 @@ function showHelp() {
2540
2558
  console.log(' inject 交互选择要执行的注入脚本');
2541
2559
  console.log(' inject-minimax 注入 MiniMax 模型配置 (可选: --key sk-xxx, -f 强制清理其他模型)');
2542
2560
  console.log(' inject-zai 注入智谱 GLM 模型配置 (可选: --key xxx, -f 强制清理其他模型)');
2561
+ console.log(' inject-claude 注入 B.AI 多模型配置 (Claude/GPT/Gemini,仅追加)');
2543
2562
  console.log(' inject-image 注入图像生成模型配置 (基于 vveai)');
2544
2563
  console.log(' inject-token 设置 Gateway Token 为 aiyiran');
2545
2564
  console.log(' inject-search 注入 Tavily 搜索插件配置');
@@ -2593,7 +2612,8 @@ if (!command) {
2593
2612
  } else if (command === 'install' || command === 'i') {
2594
2613
  runInstall();
2595
2614
  } else if (command === 'longxia') {
2596
- runReinstall();
2615
+ const isBeta = args.includes('--beta');
2616
+ runReinstall(isBeta ? OPENCLAW_VERSION_BETA : undefined, isBeta);
2597
2617
  } else if (command === 'status') {
2598
2618
  runStatus();
2599
2619
  } else if (command === 'new') {
@@ -2688,6 +2708,12 @@ if (!command) {
2688
2708
  console.log('🔄 正在重启 Gateway 使配置生效...');
2689
2709
  console.log('');
2690
2710
  runRestart();
2711
+ } else if (command === 'inject-claude') {
2712
+ const claude = require('./injects/inject-claude');
2713
+ claude.run(args.slice(1));
2714
+ console.log('🔄 正在重启 Gateway 使配置生效...');
2715
+ console.log('');
2716
+ runRestart();
2691
2717
  } else if (command === 'inject-image') {
2692
2718
  const image = require('./injects/inject-image');
2693
2719
  image.run(args.slice(1));
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * inject-claude.js
5
+ *
6
+ * 注入 B.AI 多模型配置到 openclaw.json(仅追加,不改默认)。
7
+ *
8
+ * 用法:
9
+ * myclaw inject-claude # 追加 bai provider + 白名单模型
10
+ * myclaw inject-claude --key xxx # 使用指定 API Key
11
+ */
12
+
13
+ const { readConfig, writeConfig } = require('../find-config');
14
+
15
+ const DEFAULT_BAI_KEY = "sk-28zk9p6jcwz0u9etjwayual4rmoe4pms";
16
+
17
+ const PROVIDER_ID = "bai";
18
+
19
+ const BAI_MODELS = [
20
+ {
21
+ id: "gpt-5.4-mini",
22
+ name: "GPT-5.4 Mini",
23
+ input: ["text"],
24
+ reasoning: true,
25
+ contextWindow: 200000,
26
+ maxTokens: 131072
27
+ },
28
+ {
29
+ id: "claude-sonnet-4.6",
30
+ name: "Claude Sonnet 4.6",
31
+ input: ["text"],
32
+ reasoning: true,
33
+ contextWindow: 200000,
34
+ maxTokens: 131072
35
+ },
36
+ {
37
+ id: "gemini-3.1-pro",
38
+ name: "Gemini 3.1 Pro",
39
+ input: ["text"],
40
+ reasoning: true,
41
+ contextWindow: 200000,
42
+ maxTokens: 131072
43
+ }
44
+ ];
45
+
46
+ function run(cliArgs) {
47
+ let apiKey = null;
48
+
49
+ for (let i = 0; i < cliArgs.length; i++) {
50
+ if (cliArgs[i] === '--key' && cliArgs[i + 1]) {
51
+ apiKey = cliArgs[i + 1];
52
+ i++;
53
+ }
54
+ }
55
+
56
+ if (!apiKey) {
57
+ apiKey = DEFAULT_BAI_KEY;
58
+ console.log('💡 未传入 --key,使用默认 API Key');
59
+ }
60
+
61
+ let config, configPath;
62
+ try {
63
+ ({ config, configPath } = readConfig());
64
+ } catch (err) {
65
+ console.error('❌ ' + err.message);
66
+ process.exit(1);
67
+ }
68
+
69
+ console.log('📍 找到配置: ' + configPath);
70
+ console.log('📌 模式: 追加');
71
+
72
+ // ── 注入 bai provider ──
73
+ console.log('');
74
+ console.log('📝 注入 B.AI 配置...');
75
+
76
+ if (!config.models) config.models = {};
77
+ if (!config.models.mode) config.models.mode = "merge";
78
+ if (!config.models.providers) config.models.providers = {};
79
+
80
+ config.models.providers[PROVIDER_ID] = {
81
+ baseUrl: "https://api.b.ai/v1/",
82
+ apiKey: apiKey,
83
+ api: "openai-completions",
84
+ models: BAI_MODELS
85
+ };
86
+
87
+ // ── 白名单(带 alias)──
88
+ if (!config.agents) config.agents = {};
89
+ if (!config.agents.defaults) config.agents.defaults = {};
90
+ if (!config.agents.defaults.models) config.agents.defaults.models = {};
91
+
92
+ config.agents.defaults.models["bai/gpt-5.4-mini"] = { alias: "gpt-5.4-mini" };
93
+ config.agents.defaults.models["bai/claude-sonnet-4.6"] = { alias: "claude-sonnet-4.6" };
94
+ config.agents.defaults.models["bai/gemini-3.1-pro"] = { alias: "gemini-3.1-pro" };
95
+
96
+ writeConfig(config, configPath);
97
+
98
+ console.log('✅ B.AI 注入完成');
99
+ console.log(' 模型: gpt-5.4-mini / claude-sonnet-4.6 / gemini-3.1-pro');
100
+ console.log(' 默认模型: (未修改)');
101
+ console.log('');
102
+ }
103
+
104
+ module.exports = { run };
@@ -9,6 +9,7 @@
9
9
  * myclaw inject-minimax # 仅追加 minimax provider,不改默认,副作用最小
10
10
  * myclaw inject-minimax --default # 追加 + 设为默认 + 全部对话迁移到 MiniMax-M3
11
11
  * myclaw inject-minimax --only # 追加 + 设为默认 + 全部对话迁移 + 清掉所有其他 provider
12
+ * myclaw inject-minimax --beta # --only 的全部行为 + 去掉 authHeader (适配 openclaw 6.X)
12
13
  * myclaw inject-minimax --key sk-xxx # 使用指定 API Key
13
14
  */
14
15
 
@@ -27,6 +28,7 @@ function run(cliArgs) {
27
28
  let apiKey = null;
28
29
  let setDefault = false;
29
30
  let onlyMode = false;
31
+ let betaMode = false;
30
32
 
31
33
  for (let i = 0; i < cliArgs.length; i++) {
32
34
  if (cliArgs[i] === '--key' && cliArgs[i + 1]) {
@@ -37,6 +39,10 @@ function run(cliArgs) {
37
39
  } else if (cliArgs[i] === '--only') {
38
40
  onlyMode = true;
39
41
  setDefault = true; // --only 隐含 --default
42
+ } else if (cliArgs[i] === '--beta') {
43
+ betaMode = true;
44
+ onlyMode = true; // --beta 隐含 --only
45
+ setDefault = true;
40
46
  }
41
47
  }
42
48
 
@@ -55,7 +61,7 @@ function run(cliArgs) {
55
61
  }
56
62
 
57
63
  console.log('📍 找到配置: ' + configPath);
58
- console.log('📌 模式: ' + (onlyMode ? '--only (独占)' : setDefault ? '--default (设默认)' : '追加'));
64
+ console.log('📌 模式: ' + (betaMode ? '--beta (独占+6.X兼容)' : onlyMode ? '--only (独占)' : setDefault ? '--default (设默认)' : '追加'));
59
65
 
60
66
  // ── Step 1:--only 清掉所有其他 provider ──
61
67
  if (onlyMode) {
@@ -100,10 +106,9 @@ function run(cliArgs) {
100
106
  if (!config.models.mode) config.models.mode = "merge";
101
107
  if (!config.models.providers) config.models.providers = {};
102
108
 
103
- config.models.providers.minimax = {
109
+ const minimaxProvider = {
104
110
  baseUrl: "https://api.minimaxi.com/anthropic",
105
111
  api: "anthropic-messages",
106
- authHeader: true,
107
112
  models: [
108
113
  {
109
114
  id: "MiniMax-M3",
@@ -125,6 +130,8 @@ function run(cliArgs) {
125
130
  }
126
131
  ]
127
132
  };
133
+ if (!betaMode) minimaxProvider.authHeader = true;
134
+ config.models.providers.minimax = minimaxProvider;
128
135
 
129
136
  if (!config.agents) config.agents = {};
130
137
  if (!config.agents.defaults) config.agents.defaults = {};
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * 用法:
9
9
  * myclaw inject-zai # 仅追加 zai provider,不改默认,副作用最小
10
- * myclaw inject-zai --default # 追加 + 设为默认 + 全部对话迁移到 glm-5.1
10
+ * myclaw inject-zai --default # 追加 + 设为默认 + 全部对话迁移到 glm-5.2
11
11
  * myclaw inject-zai --only # 追加 + 设为默认 + 全部对话迁移 + 清掉所有其他 provider
12
12
  * myclaw inject-zai --key xxx # 使用指定 API Key
13
13
  */
@@ -20,9 +20,18 @@ const os = require('os');
20
20
  const DEFAULT_ZAI_KEY = "a7ae7806dea8406bb85eea5d859c00ad.pVnBuagTz6942JHp";
21
21
 
22
22
  const PROVIDER_ID = "zai";
23
- const DEFAULT_MODEL = "zai/glm-5.1";
23
+ const DEFAULT_MODEL = "zai/glm-5.2";
24
24
 
25
25
  const ZAI_MODELS = [
26
+ {
27
+ id: "glm-5.2",
28
+ name: "GLM-5.2",
29
+ reasoning: true,
30
+ input: ["text"],
31
+ cost: { input: 1.2, output: 4, cacheRead: 0.24, cacheWrite: 0 },
32
+ contextWindow: 202800,
33
+ maxTokens: 131100
34
+ },
26
35
  {
27
36
  id: "glm-5.1",
28
37
  name: "GLM-5.1",
@@ -230,8 +239,9 @@ function run(cliArgs) {
230
239
  if (!config.agents.defaults) config.agents.defaults = {};
231
240
  if (!config.agents.defaults.models) config.agents.defaults.models = {};
232
241
 
233
- // 白名单:仅 glm-5.1(其余按需选)
234
- config.agents.defaults.models["zai/glm-5.1"] = {};
242
+ // 白名单:glm-5.2 + glm-5-turbo
243
+ config.agents.defaults.models["zai/glm-5.2"] = {};
244
+ config.agents.defaults.models["zai/glm-5-turbo"] = {};
235
245
 
236
246
  // ── Step 3:--default 设默认模型 ──
237
247
  if (setDefault) {
@@ -312,7 +322,8 @@ function run(cliArgs) {
312
322
  const m = sessions[key]?.model;
313
323
  if (!m) continue;
314
324
  // 修复无前缀的旧格式残留
315
- if (m === 'glm-5.1') { sessions[key].model = 'zai/glm-5.1'; changed++; continue; }
325
+ if (m === 'glm-5.1') { sessions[key].model = 'zai/glm-5.2'; changed++; continue; }
326
+ if (m === 'zai/glm-5.1') { sessions[key].model = 'zai/glm-5.2'; changed++; continue; }
316
327
  if (m === 'MiniMax-M2.7-highspeed') { sessions[key].model = 'minimax/MiniMax-M3'; changed++; continue; }
317
328
  // 统一迁移到新默认
318
329
  if (m !== DEFAULT_MODEL) { sessions[key].model = DEFAULT_MODEL; changed++; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.1.129",
3
+ "version": "1.1.131",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -5,83 +5,134 @@ description: Extract and render chat history from OpenClaw session URLs. Use whe
5
5
 
6
6
  # Chat History Extractor
7
7
 
8
- ## Workflow
8
+ ## 支持的输入方式
9
9
 
10
- ### Step 1: Parse URL and Extract Session Key
10
+ - **单个 URL/key**:处理一个会话
11
+ - **逗号分隔**:`url1,url2,url3` 批量处理
12
+ - **JSON 数组**:`["url1","url2","url3"]` 批量处理
11
13
 
12
- From URL like `https://claw1.kekouen.cn/chat?session=agent%3Ac108-v1-1811%3Amain`:
14
+ ## 快速使用(推荐)
13
15
 
14
- 1. URL-decode the `session` query parameter
15
- 2. The decoded session key is `agent:c108-v1-1811:main`
16
+ ```bash
17
+ # 单个
18
+ python3 scripts/extract_chat.py "<url>" <output-dir>
16
19
 
17
- ### Step 2: Find Session JSONL File
20
+ # 批量(逗号分隔)
21
+ python3 scripts/extract_chat.py "url1,url2,url3" <output-dir>
18
22
 
19
- Session files are stored at:
23
+ # 批量(JSON 数组)
24
+ python3 scripts/extract_chat.py '["url1","url2","url3"]' <output-dir>
20
25
  ```
21
- /root/.openclaw/agents/{agentId}/sessions/sessions.json
22
- ```
23
-
24
- For session key `agent:c108-v1-1811:main`:
25
- - Agent ID: `c108-v1-1811`
26
- - Session file: `/root/.openclaw/agents/c108-v1-1811/sessions/sessions.json`
27
-
28
- Look up the session key in `sessions.json` to find the `sessionFile` path (e.g., `5bee9664-7b72-4efd-8dc1-e8bf125c6b9c.jsonl`).
29
-
30
- ### Step 3: Parse JSONL and Build Conversation Pairs
31
26
 
32
- The JSONL contains message events. Parse them with this logic:
27
+ ## 输出文件
33
28
 
34
- 1. Filter for `type == "message"` events
35
- 2. For each user message, collect ALL consecutive assistant messages until the next user message
36
- 3. Combine multiple AI responses into one reply per user message
37
- 4. Skip `toolResult` events (they're tool outputs, not user/AI messages)
29
+ | 文件 | 说明 |
30
+ |---|---|
31
+ | `01-<session-name>.js` | 1 个会话的数据 |
32
+ | `02-<session-name>.js` | 2 个会话的数据 |
33
+ | `...` | |
34
+ | `index.js` | 所有会话的索引列表(仅批量模式生成) |
35
+ | `chat_history.js` | 向后兼容,指向第一个会话 |
38
36
 
39
- **Important pairing rules:**
40
- - A user message pairs with the first assistant message that follows
41
- - If the assistant sends multiple messages before the next user message, merge ALL of them into one AI reply
42
- - AI messages with empty text content (just tool calls) should be skipped
43
- - Use `toolResult` to skip over intermediate tool outputs
44
-
45
- ### Step 4: Generate JS Data File
46
-
47
- Output a JS file with this structure:
37
+ 每个 JS 文件结构:
48
38
 
49
39
  ```javascript
50
40
  const chatData = {
51
41
  "session": "session-name",
52
- "session_id": "uuid",
42
+ "session_id": "agent:xxx:yyy",
53
43
  "total_pairs": N,
54
- "initiator": "who started this conversation",
44
+ "initiator": "...",
55
45
  "conversations": [
56
- {
57
- "user": "full user message text",
58
- "user_time": "2026-05-14 18:11:56",
59
- "ai": "full AI reply (merged if multiple messages)",
60
- "ai_time": "2026-05-14 18:12:18"
61
- },
46
+ { "user": "...", "user_time": "...", "ai": "...", "ai_time": "..." },
62
47
  ...
63
48
  ]
64
49
  };
65
50
  ```
66
51
 
67
- ### Step 5: Render with Template HTML
52
+ `index.js` 结构:
53
+
54
+ ```javascript
55
+ const chatIndex = [
56
+ { "index": 1, "session": "...", "js_file": "01-xxx.js", "total_pairs": 10 },
57
+ { "index": 2, "session": "...", "js_file": "02-yyy.js", "total_pairs": 8 },
58
+ ];
59
+ ```
60
+
61
+ ## 工作流
62
+
63
+ ### Step 1: 解析输入
64
+
65
+ 从 URL 中提取 session key,或直接使用传入的 key。
66
+
67
+ URL 示例:`https://claw1.kekouen.cn/chat?session=agent%3Ac108-v1-1811%3Amain`
68
+ 解码后:`agent:c108-v1-1811:main`
68
69
 
69
- Use the template at `assets/chat-history-template.html` to render the conversation.
70
+ 批量输入支持逗号分隔或 JSON 数组,脚本会自动识别。
70
71
 
71
- Copy the template to the output directory alongside the JS file. The template supports:
72
- - Displaying conversation pairs
73
- - Computing and showing three timing metrics per pair:
74
- - **课程流逝** (elapsed time since first message)
75
- - **回复耗时** (AI response time = AI time - user time)
76
- - **间隔耗时** (gap to next user message)
77
- - Rich visual styling with color-coded timing badges
72
+ ### Step 2: 定位 JSONL 文件
73
+
74
+ ```
75
+ /root/.openclaw/agents/{agentId}/sessions/sessions.json
76
+ ```
78
77
 
79
- ## Usage Example
78
+ session key 在 `sessions.json` 中查找 `sessionFile` 路径。
80
79
 
81
- User provides: `https://claw1.kekouen.cn/chat?session=agent%3Ac108-v1-1811%3Amain`
80
+ ### Step 3: 解析 JSONL,构建对话对
81
+
82
+ 1. 筛选 `type == "message"` 的事件
83
+ 2. 每条用户消息与后续**所有连续的** AI 消息配对
84
+ 3. 多条 AI 回复合并为一条
85
+ 4. 跳过 `toolResult` 事件和空文本消息
86
+
87
+ ### Step 4: 生成 JS 文件
88
+
89
+ 批量模式下,每个会话生成独立的 `NN-<name>.js`,并额外生成 `index.js` 索引。
90
+
91
+ ### Step 5: 渲染
92
+
93
+ 将模板 `assets/chat-history-template.html` 复制到输出目录。
94
+ 模板会加载 `chat_history.js`(单会话)或可通过 `index.js` 切换(多会话)。
95
+
96
+ 模板展示三种时间指标:
97
+ - **课程流逝** — 距首条消息的时间
98
+ - **回复耗时** — AI 响应时间
99
+ - **间隔耗时** — 到下一条用户消息的间隔
100
+
101
+ ## Agent 手动流程
102
+
103
+ 如果不使用脚本(如当前 agent 直接操作):
104
+
105
+ 1. **收集**:解析 URL → 查 `sessions.json` → 复制 JSONL 到输出目录
106
+ 2. **运行脚本**:`python3 scripts/extract_chat.py "<urls>" <output-dir>`
107
+ 3. **渲染**:复制 HTML 模板到输出目录,用浏览器打开
108
+
109
+ ## 示例
110
+
111
+ ### 单个会话
112
+
113
+ ```bash
114
+ python3 scripts/extract_chat.py "https://claw1.kekouen.cn/chat?session=agent%3Ac108-v1-1811%3Amain" ./output
115
+ ```
116
+
117
+ 输出:
118
+ - `01-main.js`
119
+ - `chat_history.js`(= 01-main.js 的副本)
120
+
121
+ ### 批量处理
122
+
123
+ ```bash
124
+ python3 scripts/extract_chat.py '[
125
+ "https://claw6.kekouen.cn/chat?session=agent%3Axuexiji%3Amain",
126
+ "https://claw6.kekouen.cn/chat?session=agent%3Ausa%3Amain",
127
+ "https://claw6.kekouen.cn/chat?session=agent%3Ac109-v3-1813%3A...",
128
+ "https://claw6.kekouen.cn/chat?session=agent%3Ac108-v1-1810%3A..."
129
+ ]' ./output
130
+ ```
82
131
 
83
- 1. Extract session key: `agent:c108-v1-1811:main`
84
- 2. Find JSONL at: `/root/.openclaw/agents/c108-v1-1811/sessions/5bee9664-7b72-4efd-8dc1-e8bf125c6b9c.jsonl`
85
- 3. Parse and generate: `聊天记录.js`
86
- 4. Copy template: `聊天记录.html`
87
- 5. User opens `聊天记录.html` to view the rendered conversation
132
+ 输出:
133
+ - `01-main.js`
134
+ - `02-main.js`
135
+ - `03-每天一个小习惯·宠物养成_1813.js`
136
+ - `04-我的金币任务板_1810.js`
137
+ - `index.js`
138
+ - `chat_history.js`(= 01-main.js 的副本)
@@ -1,7 +1,21 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Extract chat history from OpenClaw session URL and generate JS data file.
4
- Usage: python3 extract_chat.py <session-url-or-key> [output-dir]
3
+ Extract chat history from OpenClaw session URL(s) and generate JS data file(s).
4
+
5
+ Usage:
6
+ # 单个会话
7
+ python3 extract_chat.py <session-url-or-key> [output-dir]
8
+
9
+ # 多个会话(逗号分隔)
10
+ python3 extract_chat.py "url1,url2,url3" [output-dir]
11
+
12
+ # 多个会话(JSON 数组)
13
+ python3 extract_chat.py '["url1","url2","url3"]' [output-dir]
14
+
15
+ 输出:
16
+ - 每个会话生成独立的 <index>-<session-name>.js
17
+ - 生成 index.js,包含所有会话的元信息列表
18
+ - 生成 chat_history.js(向后兼容,指向第一个会话)
5
19
  """
6
20
 
7
21
  import json
@@ -13,6 +27,7 @@ from datetime import datetime, timezone, timedelta
13
27
 
14
28
  tz_beijing = timezone(timedelta(hours=8))
15
29
 
30
+
16
31
  def parse_time(ts_str):
17
32
  """Parse ISO timestamp string to Beijing time string."""
18
33
  try:
@@ -24,6 +39,7 @@ def parse_time(ts_str):
24
39
  except:
25
40
  return None
26
41
 
42
+
27
43
  def extract_session_key(url_or_key):
28
44
  """Extract session key from URL or return as-is if already a key."""
29
45
  if 'session=' in url_or_key:
@@ -33,29 +49,56 @@ def extract_session_key(url_or_key):
33
49
  return params['session'][0]
34
50
  return url_or_key
35
51
 
52
+
53
+ def parse_input(input_str):
54
+ """
55
+ Parse input into a list of session URLs/keys.
56
+ Supports: comma-separated, JSON array, or single value.
57
+ """
58
+ input_str = input_str.strip()
59
+
60
+ # Try JSON array
61
+ if input_str.startswith('['):
62
+ try:
63
+ arr = json.loads(input_str)
64
+ return [item.strip() for item in arr if isinstance(item, str) and item.strip()]
65
+ except json.JSONDecodeError:
66
+ pass
67
+
68
+ # Comma-separated (but not commas inside URL-encoded params like %3A)
69
+ # URLs use %3A for colon, so raw commas are safe as separators
70
+ parts = [p.strip() for p in input_str.split(',') if p.strip()]
71
+ return parts if len(parts) > 1 else [input_str]
72
+
73
+
36
74
  def find_session_file(session_key):
37
75
  """Find the JSONL file path for a given session key."""
38
- # session_key format: agent:c108-v1-1811:session-name
39
76
  parts = session_key.split(':')
40
77
  if len(parts) < 2:
41
78
  return None
42
79
  agent_id = parts[1]
43
80
  sessions_json_path = f'/root/.openclaw/agents/{agent_id}/sessions/sessions.json'
44
-
81
+
45
82
  if not os.path.exists(sessions_json_path):
46
83
  return None
47
-
84
+
48
85
  with open(sessions_json_path, 'r') as f:
49
86
  sessions = json.load(f)
50
-
87
+
51
88
  if session_key in sessions:
52
- return sessions[session_key].get('sessionFile')
89
+ session_file = sessions[session_key].get('sessionFile')
90
+ if session_file and not os.path.isabs(session_file):
91
+ session_file = os.path.join(
92
+ f'/root/.openclaw/agents/{agent_id}/sessions', session_file
93
+ )
94
+ return session_file
53
95
  return None
54
96
 
97
+
55
98
  def parse_jsonl_to_conversations(jsonl_path):
56
99
  """Parse JSONL file and build conversation pairs."""
57
100
  messages = []
58
- with open(jsonl_path, 'r') as f:
101
+ with open(jsonl_path, 'r', encoding='utf-8') as f:
59
102
  for line in f:
60
103
  line = line.strip()
61
104
  if not line:
@@ -65,19 +108,21 @@ def parse_jsonl_to_conversations(jsonl_path):
65
108
  role = obj['message']['role']
66
109
  content = obj['message'].get('content', [])
67
110
  timestamp = obj.get('timestamp', '')
68
-
111
+
69
112
  if isinstance(content, list):
70
- text = ''.join([c.get('text', '') for c in content if c.get('type') == 'text'])
113
+ text = ''.join([
114
+ c.get('text', '') for c in content if c.get('type') == 'text'
115
+ ])
71
116
  else:
72
117
  text = str(content)
73
-
118
+
74
119
  messages.append({
75
120
  'role': role,
76
121
  'timestamp': timestamp,
77
122
  'text': text.strip(),
78
123
  'has_text': bool(text.strip())
79
124
  })
80
-
125
+
81
126
  # Build conversation pairs
82
127
  conversations = []
83
128
  i = 0
@@ -86,19 +131,17 @@ def parse_jsonl_to_conversations(jsonl_path):
86
131
  if msg['role'] == 'user':
87
132
  user_text = msg['text']
88
133
  user_time = msg['timestamp']
89
-
90
- # Collect ALL consecutive AI messages until next user
134
+
91
135
  ai_messages = []
92
136
  j = i + 1
93
137
  while j < len(messages) and messages[j]['role'] != 'user':
94
138
  if messages[j]['role'] == 'assistant':
95
139
  ai_messages.append(messages[j])
96
140
  j += 1
97
-
98
- # Merge all AI messages with text
141
+
99
142
  ai_text = '\n\n'.join([m['text'] for m in ai_messages if m['has_text']])
100
143
  ai_time = ai_messages[-1]['timestamp'] if ai_messages else ''
101
-
144
+
102
145
  conversations.append({
103
146
  'user': user_text,
104
147
  'user_time': parse_time(user_time) if user_time else '',
@@ -108,13 +151,22 @@ def parse_jsonl_to_conversations(jsonl_path):
108
151
  i += 1
109
152
  else:
110
153
  i += 1
111
-
154
+
112
155
  return conversations
113
156
 
157
+
158
+ def sanitize_filename(name):
159
+ """Make a safe filename from session name."""
160
+ # Keep Chinese chars, alphanumerics, replace others with -
161
+ safe = re.sub(r'[^\w\u4e00-\u9fff\u3400-\u4dbf-]', '-', name)
162
+ safe = re.sub(r'-+', '-', safe).strip('-')
163
+ return safe or 'session'
164
+
165
+
114
166
  def generate_js(conversations, session_key, output_path):
115
167
  """Generate JS file from conversations."""
116
168
  session_name = session_key.split(':')[-1] if ':' in session_key else session_key
117
-
169
+
118
170
  output_data = {
119
171
  'session': session_name,
120
172
  'session_id': session_key,
@@ -122,48 +174,127 @@ def generate_js(conversations, session_key, output_path):
122
174
  'initiator': 'session initiator',
123
175
  'conversations': conversations
124
176
  }
125
-
177
+
126
178
  js_content = f'''// Chat History - {session_name}
127
179
  // Session Key: {session_key}
128
180
  // Generated by chat-history-extractor skill
129
181
 
130
182
  const chatData = {json.dumps(output_data, ensure_ascii=False, indent=2)};
131
183
  '''
132
-
184
+
133
185
  with open(output_path, 'w', encoding='utf-8') as f:
134
186
  f.write(js_content)
135
-
136
- return len(conversations)
137
187
 
138
- def main():
139
- if len(sys.argv) < 2:
140
- print("Usage: python3 extract_chat.py <session-url-or-key> [output-dir]")
141
- sys.exit(1)
142
-
143
- url_or_key = sys.argv[1]
144
- output_dir = sys.argv[2] if len(sys.argv) > 2 else os.getcwd()
145
-
146
- # Extract session key
188
+ return len(conversations), session_name
189
+
190
+
191
+ def process_one(url_or_key, output_dir, index):
192
+ """
193
+ Process a single session URL/key.
194
+ Returns dict with results or None on failure.
195
+ """
147
196
  session_key = extract_session_key(url_or_key)
148
- print(f"Session key: {session_key}")
149
-
150
- # Find session file
197
+ session_short = sanitize_filename(session_key.split(':')[-1] if ':' in session_key else session_key)
198
+ print(f"\n[{index}] Session key: {session_key}")
199
+
151
200
  jsonl_path = find_session_file(session_key)
152
201
  if not jsonl_path:
153
- print(f"ERROR: Could not find session file for {session_key}")
154
- sys.exit(1)
155
- print(f"JSONL path: {jsonl_path}")
156
-
157
- # Parse conversations
202
+ print(f"[{index}] ERROR: Could not find session file for {session_key}")
203
+ return None
204
+
205
+ if not os.path.exists(jsonl_path):
206
+ print(f"[{index}] ERROR: JSONL file not found: {jsonl_path}")
207
+ return None
208
+
209
+ print(f"[{index}] JSONL: {jsonl_path}")
158
210
  conversations = parse_jsonl_to_conversations(jsonl_path)
159
- print(f"Found {len(conversations)} conversation pairs")
160
-
161
- # Generate JS file
211
+ print(f"[{index}] Found {len(conversations)} conversation pairs")
212
+
213
+ # Generate per-session JS with index prefix
214
+ js_filename = f"{index:02d}-{session_short}.js"
215
+ js_path = os.path.join(output_dir, js_filename)
216
+ count, session_name = generate_js(conversations, session_key, js_path)
217
+ print(f"[{index}] Generated: {js_filename} ({count} pairs)")
218
+
219
+ return {
220
+ 'index': index,
221
+ 'session_key': session_key,
222
+ 'session_name': session_name,
223
+ 'js_file': js_filename,
224
+ 'total_pairs': count,
225
+ 'jsonl_path': jsonl_path,
226
+ }
227
+
228
+
229
+ def generate_index_js(results, output_dir):
230
+ """Generate an index.js listing all sessions."""
231
+ sessions = []
232
+ for r in results:
233
+ sessions.append({
234
+ 'index': r['index'],
235
+ 'session': r['session_name'],
236
+ 'session_id': r['session_key'],
237
+ 'js_file': r['js_file'],
238
+ 'total_pairs': r['total_pairs'],
239
+ })
240
+
241
+ index_content = f'''// Chat History Index - {len(sessions)} session(s)
242
+ // Generated by chat-history-extractor skill
243
+
244
+ const chatIndex = {json.dumps(sessions, ensure_ascii=False, indent=2)};
245
+ '''
246
+ index_path = os.path.join(output_dir, 'index.js')
247
+ with open(index_path, 'w', encoding='utf-8') as f:
248
+ f.write(index_content)
249
+ print(f"\nIndex: {index_path} ({len(sessions)} sessions)")
250
+
251
+
252
+ def main():
253
+ if len(sys.argv) < 2:
254
+ print("Usage:")
255
+ print(" python3 extract_chat.py <url-or-key> [output-dir]")
256
+ print(" python3 extract_chat.py 'url1,url2,url3' [output-dir]")
257
+ print(' python3 extract_chat.py \'["url1","url2"]\' [output-dir]')
258
+ sys.exit(1)
259
+
260
+ raw_input = sys.argv[1]
261
+ output_dir = sys.argv[2] if len(sys.argv) > 2 else os.getcwd()
262
+
263
+ # Parse input into list
264
+ items = parse_input(raw_input)
265
+ print(f"Input: {len(items)} session(s)")
266
+
162
267
  os.makedirs(output_dir, exist_ok=True)
163
- js_path = os.path.join(output_dir, 'chat_history.js')
164
- count = generate_js(conversations, session_key, js_path)
165
- print(f"Generated: {js_path}")
166
- print(f"Total pairs: {count}")
268
+
269
+ # Process each session
270
+ results = []
271
+ for i, item in enumerate(items, 1):
272
+ r = process_one(item, output_dir, i)
273
+ if r:
274
+ results.append(r)
275
+
276
+ if not results:
277
+ print("\nERROR: No sessions processed successfully.")
278
+ sys.exit(1)
279
+
280
+ # Generate index.js if multiple sessions
281
+ if len(results) > 1:
282
+ generate_index_js(results, output_dir)
283
+
284
+ # Backward compat: copy first session as chat_history.js
285
+ first_js = os.path.join(output_dir, results[0]['js_file'])
286
+ compat_js = os.path.join(output_dir, 'chat_history.js')
287
+ if first_js != compat_js:
288
+ import shutil
289
+ shutil.copy2(first_js, compat_js)
290
+ print(f"Compat: chat_history.js -> {results[0]['js_file']}")
291
+
292
+ # Summary
293
+ print(f"\n{'='*50}")
294
+ print(f"Done! {len(results)}/{len(items)} session(s) processed.")
295
+ for r in results:
296
+ print(f" [{r['index']}] {r['session_name']} ({r['total_pairs']} pairs) -> {r['js_file']}")
297
+
167
298
 
168
299
  if __name__ == '__main__':
169
300
  main()