agent-bridge-chatgpt 1.0.0 → 1.0.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 CHANGED
@@ -29,3 +29,19 @@ Start-Process -FilePath $executable -ArgumentList '--remote-debugging-port=9230'
29
29
  ```
30
30
 
31
31
  插件启动时会建立 SSH 转发,并连接本机 `127.0.0.1:9230`。不配置 SSH 时,可直接连接本机已开启 CDP 的 ChatGPT 页面。
32
+
33
+ ## 最近任务与继续会话
34
+
35
+ 插件会在自己的数据目录保存最多 20 条最近 ChatGPT 会话索引(不保存账号、密码或页面 HTML)。Channel 查询以下内容时优先读取本地缓存,缓存过期后才同步一次远程侧栏:
36
+
37
+ - `最近任务`
38
+ - `列出最近会话`
39
+ - `有哪些已保存的 ChatGPT 任务`
40
+
41
+ 可以使用 `切换会话:任务标题` 或 `继续处理任务标题` 切换到已有会话。切换成功后,同一 Channel 的后续消息会继续发送到该会话;标题重复时插件会返回候选列表,避免误切换。发送“刷新最近任务”可以强制同步侧栏。
42
+
43
+ 缓存默认有效期为 10 分钟,可跨插件重启恢复,并按远程连接隔离。列表覆盖当前已加载的侧栏项目,不代表全部历史会话,也无法推断远程所有任务的精确更新时间。需要其他项目时,先在桌面侧栏展开后刷新列表。
44
+
45
+ `切换会话:任务标题` 只切换;`切换会话:任务标题;检查进度` 会切换并发送后半句;`继续处理任务标题` 会切换并向该会话发送这条继续指令。也可用列表中的完整会话 ID 指定目标。当前会话有未发送草稿或正在生成回复时,插件拒绝切换。
46
+
47
+ 索引文件为插件数据目录下的 `chatgpt-sessions.json`,记录最近使用时间和最多 500 字符的最近消息/回复片段。它用于检索会话,不自动生成 Skill;远程暂时不可用时仍可查询已有缓存。
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveRuntimePort = resolveRuntimePort;
4
+ function resolveRuntimePort(config, runtime, pluginId, envName, fallback) {
5
+ const injected = runtime?.ports?.[pluginId];
6
+ if (typeof injected === "number" && Number.isInteger(injected) && injected > 0)
7
+ return injected;
8
+ const configured = config.cdpPort;
9
+ if (typeof configured === "number" && Number.isInteger(configured) && configured > 0)
10
+ return configured;
11
+ const environment = Number(process.env[envName] ?? "");
12
+ return Number.isInteger(environment) && environment > 0 ? environment : fallback;
13
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ChatgptCdpClient = exports.userBubbleSelector = exports.turnSelector = exports.editorSelector = void 0;
4
4
  const node_child_process_1 = require("node:child_process");
5
+ const session_store_1 = require("./session-store");
5
6
  exports.editorSelector = '[data-codex-composer="true"][contenteditable="true"]';
6
7
  exports.turnSelector = '[data-chatgpt-conversation-turn="true"]';
7
8
  exports.userBubbleSelector = "[data-user-message-bubble]";
@@ -11,6 +12,7 @@ const contentUnitSelector = "[data-content-search-unit-key]";
11
12
  const sendButtonSelector = 'button[aria-label="Send"], button[aria-label="发送"]';
12
13
  const stopButtonSelector = 'button[aria-label="Stop"], button[aria-label="停止"]';
13
14
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
+ const sidebarSelector = "[data-app-action-sidebar-thread-id]";
14
16
  class ChatgptCdpClient {
15
17
  socket;
16
18
  tunnel;
@@ -29,6 +31,51 @@ class ChatgptCdpClient {
29
31
  this.ssh = options.ssh;
30
32
  }
31
33
  connected() { return Boolean(this.socket); }
34
+ async listSessions() {
35
+ const entries = await this.evaluate(`(() => {
36
+ return [...document.querySelectorAll(${JSON.stringify(sidebarSelector)})].map(item => ({
37
+ id: item.getAttribute('data-app-action-sidebar-thread-id'),
38
+ host: item.getAttribute('data-app-action-sidebar-thread-host-id'),
39
+ title: item.getAttribute('data-app-action-sidebar-thread-title')
40
+ }));
41
+ })()`);
42
+ if (!Array.isArray(entries))
43
+ throw new Error("ChatGPT 侧栏会话列表格式无效");
44
+ return entries.filter(session_store_1.validSession);
45
+ }
46
+ async activeSession() {
47
+ const entry = await this.evaluate(`(() => {
48
+ const item = document.querySelector('[data-app-action-sidebar-thread-selected="true"]');
49
+ return item ? {
50
+ id: item.getAttribute('data-app-action-sidebar-thread-id'),
51
+ host: item.getAttribute('data-app-action-sidebar-thread-host-id'),
52
+ title: item.getAttribute('data-app-action-sidebar-thread-title')
53
+ } : null;
54
+ })()`);
55
+ return (0, session_store_1.validSession)(entry) ? entry : undefined;
56
+ }
57
+ async switchSession(session) {
58
+ const locate = `[...document.querySelectorAll(${JSON.stringify(sidebarSelector)})].find(item =>
59
+ item.getAttribute('data-app-action-sidebar-thread-id') === ${JSON.stringify(session.id)}
60
+ && item.getAttribute('data-app-action-sidebar-thread-host-id') === ${JSON.stringify(session.host)})`;
61
+ const clicked = await this.evaluate(`(() => {
62
+ const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
63
+ if (editor?.innerText.trim() || document.querySelector(${JSON.stringify(stopButtonSelector)})) {
64
+ throw new Error('当前会话有未发送内容或正在生成回复,不能切换');
65
+ }
66
+ const item = ${locate};
67
+ if (!item) return false;
68
+ item.click();
69
+ return true;
70
+ })()`);
71
+ if (!clicked)
72
+ throw new Error("ChatGPT 侧栏中找不到目标会话");
73
+ await this.waitFor(`(() => {
74
+ const item = ${locate};
75
+ return item?.getAttribute('data-app-action-sidebar-thread-selected') === 'true'
76
+ && Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}));
77
+ })()`, "ChatGPT 会话切换未确认,未发送消息");
78
+ }
32
79
  async connect() {
33
80
  if (this.socket)
34
81
  return;
@@ -2,9 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ChatgptCdpClient = exports.ChatgptPlugin = exports.chatgptManifest = void 0;
4
4
  const cdp_client_1 = require("./cdp-client");
5
+ const session_store_1 = require("./session-store");
5
6
  exports.chatgptManifest = {
6
7
  id: "chatgpt",
7
- version: "1.0.0",
8
+ version: "1.0.1",
8
9
  displayName: "ChatGPT(Codex)",
9
10
  type: "agent",
10
11
  entry: "dist/index.js",
@@ -25,6 +26,8 @@ class ChatgptPlugin {
25
26
  failure;
26
27
  remote = false;
27
28
  queue = Promise.resolve();
29
+ store;
30
+ logger;
28
31
  constructor(options = {}) {
29
32
  this.options = options;
30
33
  }
@@ -48,6 +51,11 @@ class ChatgptPlugin {
48
51
  localPort: port(config.localCdpPort, 9230),
49
52
  } : undefined;
50
53
  this.remote = Boolean(ssh);
54
+ this.logger = context.logger;
55
+ this.store = new session_store_1.ChatgptSessionStore(context.dataDir, JSON.stringify(ssh
56
+ ? [ssh.host, ssh.user, ssh.port, ssh.remotePort]
57
+ : ["local", port(config.localCdpPort, 9230)]));
58
+ await this.store.load();
51
59
  this.failure = undefined;
52
60
  this.client = this.options.client ?? new cdp_client_1.ChatgptCdpClient({
53
61
  port: port(config.localCdpPort, 9230),
@@ -60,11 +68,13 @@ class ChatgptPlugin {
60
68
  catch (error) {
61
69
  this.failure = error instanceof Error ? error.message : String(error);
62
70
  context.logger.warn(this.failure);
63
- throw error;
71
+ if (!this.store.sessions.length)
72
+ throw error;
64
73
  }
65
74
  }
66
75
  async deactivate() {
67
76
  await this.queue;
77
+ await this.store?.drain();
68
78
  this.client?.close();
69
79
  this.client = undefined;
70
80
  this.remote = false;
@@ -72,17 +82,111 @@ class ChatgptPlugin {
72
82
  async health() {
73
83
  return this.client?.connected()
74
84
  ? { status: "ready", details: { transport: "cdp", remote: this.remote } }
75
- : { status: "failed", message: this.failure ?? "ChatGPT CDP 未连接", details: { transport: "cdp", remote: this.remote } };
85
+ : { status: this.client && this.store?.sessions.length ? "degraded" : "failed", message: this.failure ?? "ChatGPT CDP 未连接", details: { transport: "cdp", remote: this.remote } };
76
86
  }
77
87
  async handle(request) {
78
88
  if (!request.task.trim())
79
89
  throw new Error("消息内容不能为空");
80
- if (!this.client?.connected())
81
- throw new Error("ChatGPT CDP 未连接");
82
- const run = this.queue.then(() => this.client.direct(request.task, request.timeoutMs));
90
+ if (!this.client || !this.store)
91
+ throw new Error("ChatGPT 插件未启动");
92
+ const run = this.queue.then(() => this.run(request));
83
93
  this.queue = run.then(() => undefined, () => undefined);
84
94
  return { text: await run, sessionId: request.sessionId };
85
95
  }
96
+ persist() {
97
+ void this.store.save().catch(error => this.logger?.warn("ChatGPT 会话缓存保存失败", { error: String(error) }));
98
+ }
99
+ async synchronize() {
100
+ if (!this.client.connected())
101
+ await this.client.connect();
102
+ if (!this.client.listSessions)
103
+ throw new Error("当前 ChatGPT 客户端不支持会话列表");
104
+ this.store.synchronize(await this.client.listSessions());
105
+ this.persist();
106
+ }
107
+ async select(session) {
108
+ if (!this.client.connected())
109
+ await this.client.connect();
110
+ if (!this.client.switchSession)
111
+ throw new Error("当前 ChatGPT 客户端不支持会话切换");
112
+ const active = await this.client.activeSession?.();
113
+ if (active?.id === session.id && active.host === session.host)
114
+ return;
115
+ try {
116
+ await this.client.switchSession(session);
117
+ }
118
+ catch (error) {
119
+ // 目标消失时只重新同步一次;不在其他会话中代替发送。
120
+ if (!String(error).includes("找不到目标会话"))
121
+ throw error;
122
+ await this.synchronize();
123
+ const updated = this.store.sessions.find(item => item.id === session.id && item.host === session.host);
124
+ if (!updated)
125
+ throw new Error("ChatGPT 目标会话已删除或不在当前侧栏,请刷新最近任务");
126
+ await this.client.switchSession(updated);
127
+ }
128
+ }
129
+ async run(request) {
130
+ const store = this.store;
131
+ const text = request.task.trim();
132
+ const refresh = /^(?:刷新最近任务|刷新任务列表|刷新最近会话)$/.test(text);
133
+ const listing = /^(?:最近任务|列出最近会话|有哪些已保存的 ChatGPT 任务)[??。]?$/.test(text);
134
+ if (refresh || listing) {
135
+ if (refresh || !store.fresh()) {
136
+ try {
137
+ await this.synchronize();
138
+ }
139
+ catch (error) {
140
+ if (refresh || !store.sessions.length)
141
+ throw error;
142
+ return `远程暂不可用,以下是本地缓存(同步时间:${store.updatedAt}):\n${(0, session_store_1.formatSessions)(store.sessions)}`;
143
+ }
144
+ }
145
+ return `最近会话(当前侧栏缓存;同步时间:${store.updatedAt}):\n${(0, session_store_1.formatSessions)(store.sessions)}`;
146
+ }
147
+ const explicit = text.match(/^切换会话\s*[::]\s*([^;;\n]+)(?:[;;\n]\s*([\s\S]+))?$/);
148
+ const natural = text.match(/^(?:继续处理|接着处理|继续任务|切换到)\s*(.+?)[。]?$/);
149
+ const query = explicit?.[1].trim() ?? natural?.[1].trim();
150
+ let selected;
151
+ if (query) {
152
+ if (!store.fresh())
153
+ await this.synchronize();
154
+ let matches = store.match(query);
155
+ if (!matches.length) {
156
+ await this.synchronize();
157
+ matches = store.match(query);
158
+ }
159
+ if (!matches.length)
160
+ return `没有找到会话:${query}。请发送“最近任务”查看可用标题。`;
161
+ if (matches.length > 1)
162
+ return `找到多个会话,请用完整标题或会话 ID 指定:\n${(0, session_store_1.formatSessions)(matches)}`;
163
+ selected = matches[0];
164
+ await this.select(selected);
165
+ if (request.sessionId)
166
+ store.bindings[request.sessionId] = selected;
167
+ this.persist();
168
+ if ((explicit && !explicit[2]) || text.startsWith("切换到"))
169
+ return `已切换到:${selected.title}。后续消息将继续此会话。`;
170
+ }
171
+ else if (request.sessionId && store.bindings[request.sessionId]) {
172
+ selected = store.bindings[request.sessionId];
173
+ await this.select(selected);
174
+ }
175
+ if (!this.client.connected())
176
+ await this.client.connect();
177
+ selected ??= await this.client.activeSession?.();
178
+ const message = explicit?.[2]?.trim() || request.task;
179
+ const reply = await this.client.direct(message, request.timeoutMs);
180
+ if (selected) {
181
+ const current = await this.client.activeSession?.().catch(() => undefined);
182
+ const entry = current?.id === selected.id && current.host === selected.host ? current : selected;
183
+ store.remember(entry, message, reply);
184
+ if (request.sessionId)
185
+ store.bindings[request.sessionId] = entry;
186
+ this.persist();
187
+ }
188
+ return reply;
189
+ }
86
190
  }
87
191
  exports.ChatgptPlugin = ChatgptPlugin;
88
192
  var cdp_client_2 = require("./cdp-client");
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChatgptSessionStore = void 0;
4
+ exports.validSession = validSession;
5
+ exports.formatSessions = formatSessions;
6
+ const promises_1 = require("node:fs/promises");
7
+ const node_path_1 = require("node:path");
8
+ function validSession(value) {
9
+ if (!value || typeof value !== "object")
10
+ return false;
11
+ const item = value;
12
+ return ["id", "host", "title"].every(key => typeof item[key] === "string" && Boolean(item[key]))
13
+ && ["updatedAt", "lastUserMessage", "lastAssistantReply"].every(key => item[key] === undefined || typeof item[key] === "string");
14
+ }
15
+ class ChatgptSessionStore {
16
+ directory;
17
+ endpoint;
18
+ sessions = [];
19
+ bindings = {};
20
+ updatedAt = "";
21
+ writes = Promise.resolve();
22
+ constructor(directory, endpoint) {
23
+ this.directory = directory;
24
+ this.endpoint = endpoint;
25
+ }
26
+ async load() {
27
+ if (!this.directory)
28
+ return;
29
+ try {
30
+ const value = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.directory, "chatgpt-sessions.json"), "utf8"));
31
+ if (value.version !== 1 || value.endpoint !== this.endpoint || !Array.isArray(value.sessions)
32
+ || !value.sessions.every(validSession) || typeof value.updatedAt !== "string"
33
+ || !value.bindings || typeof value.bindings !== "object" || Array.isArray(value.bindings)
34
+ || !Object.values(value.bindings).every(validSession))
35
+ return;
36
+ this.sessions = value.sessions.slice(0, 20);
37
+ this.bindings = Object.fromEntries(Object.entries(value.bindings));
38
+ this.updatedAt = value.updatedAt;
39
+ }
40
+ catch (error) {
41
+ if (error.code !== "ENOENT" && !(error instanceof SyntaxError))
42
+ throw error;
43
+ }
44
+ }
45
+ fresh() { return Date.now() - Date.parse(this.updatedAt) < 10 * 60_000; }
46
+ synchronize(entries) {
47
+ const previous = this.sessions;
48
+ const seen = new Set();
49
+ this.sessions = entries.filter(validSession).filter(item => {
50
+ const key = JSON.stringify([item.host, item.id]);
51
+ if (seen.has(key))
52
+ return false;
53
+ seen.add(key);
54
+ return true;
55
+ }).map(item => ({ ...previous.find(old => old.id === item.id && old.host === item.host), ...item }))
56
+ .sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")).slice(0, 20);
57
+ this.updatedAt = new Date().toISOString();
58
+ }
59
+ remember(entry, user, reply) {
60
+ const next = { ...entry, lastUserMessage: user.slice(0, 500), lastAssistantReply: reply.slice(0, 500), updatedAt: new Date().toISOString() };
61
+ this.sessions = [next, ...this.sessions.filter(old => old.id !== entry.id || old.host !== entry.host)].slice(0, 20);
62
+ }
63
+ match(query) {
64
+ const text = query.trim().toLocaleLowerCase();
65
+ if (!text)
66
+ return [];
67
+ const exact = this.sessions.filter(item => item.id === query || item.title.toLocaleLowerCase() === text);
68
+ return exact.length ? exact : this.sessions.filter(item => `${item.title}\n${item.lastUserMessage ?? ""}`.toLocaleLowerCase().includes(text));
69
+ }
70
+ save() {
71
+ const snapshot = { version: 1, endpoint: this.endpoint, updatedAt: this.updatedAt, sessions: this.sessions, bindings: this.bindings };
72
+ const text = `${JSON.stringify(snapshot, null, 2)}\n`;
73
+ const write = this.writes.catch(() => { }).then(async () => {
74
+ if (!this.directory)
75
+ return;
76
+ await (0, promises_1.mkdir)(this.directory, { recursive: true });
77
+ const file = (0, node_path_1.join)(this.directory, "chatgpt-sessions.json");
78
+ await (0, promises_1.writeFile)(`${file}.tmp`, text, { mode: 0o600 });
79
+ await (0, promises_1.rename)(`${file}.tmp`, file);
80
+ });
81
+ this.writes = write;
82
+ return write;
83
+ }
84
+ async drain() { await this.writes.catch(() => { }); }
85
+ }
86
+ exports.ChatgptSessionStore = ChatgptSessionStore;
87
+ function formatSessions(entries) {
88
+ if (!entries.length)
89
+ return "当前侧栏暂无可用会话。";
90
+ return entries.map((item, index) => `${index + 1}. ${item.title}\n 主机:${item.host};会话:${item.id}${item.updatedAt ? `;最近使用:${item.updatedAt}` : ""}`).join("\n");
91
+ }
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"agent-bridge-chatgpt","version":"1.0.0","description":"agent-bridge 的 ChatGPT Codex CDP 插件。","keywords":["mu-agent-bridge-plugin","mu-agent-bridge-agent"],"main":"dist/src-ts/plugins/chatgpt/index.js","files":["dist/","plugin.json","config.schema.json","README.md"],"agentBridge":{"plugin":"./plugin.json","type":"agent"},"engines":{"node":">=22"}}
1
+ {"name":"agent-bridge-chatgpt","version":"1.0.1","description":"agent-bridge 的 ChatGPT Codex CDP 插件。","keywords":["mu-agent-bridge-plugin","mu-agent-bridge-agent"],"main":"dist/src-ts/plugins/chatgpt/index.js","files":["dist/","plugin.json","config.schema.json","README.md"],"agentBridge":{"plugin":"./plugin.json","type":"agent"},"engines":{"node":">=22"}}
package/plugin.json CHANGED
@@ -1 +1 @@
1
- {"id":"chatgpt","version":"1.0.0","type":"agent","displayName":"ChatGPT(Codex)","entry":"dist/src-ts/plugins/chatgpt/index.js","apiVersion":"1","process":"in-process","capabilities":["chat","health"],"configSchema":"config.schema.json"}
1
+ {"id":"chatgpt","version":"1.0.1","type":"agent","displayName":"ChatGPT(Codex)","entry":"dist/src-ts/plugins/chatgpt/index.js","apiVersion":"1","process":"in-process","capabilities":["chat","health"],"configSchema":"config.schema.json"}