@yvhitxcel/opencode-remote 0.16.2 → 0.17.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.
@@ -0,0 +1,73 @@
1
+ const pendingDecisions = new Map();
2
+
3
+ export function hasPendingDecision(threadId) {
4
+ return pendingDecisions.has(threadId);
5
+ }
6
+
7
+ export function resolveDecision(threadId, input) {
8
+ const pd = pendingDecisions.get(threadId);
9
+ if (!pd) return false;
10
+ const num = parseInt(input, 10);
11
+ if (isNaN(num) || num < 1 || num > pd.options.length) return false;
12
+ clearTimeout(pd.timer);
13
+ pendingDecisions.delete(threadId);
14
+ pd.resolve(num);
15
+ return true;
16
+ }
17
+
18
+ export function sendDecision({ adapter, threadId, question, options, recommended, timeoutMs = 5 * 60 * 1000, broadcastTo = [] }) {
19
+ return new Promise((resolve) => {
20
+ if (pendingDecisions.has(threadId)) {
21
+ pendingDecisions.get(threadId).resolve(0);
22
+ clearTimeout(pendingDecisions.get(threadId).timer);
23
+ pendingDecisions.delete(threadId);
24
+ }
25
+
26
+ const lines = [
27
+ `🤖 需要你决定:`,
28
+ `${question}`,
29
+ ``,
30
+ ];
31
+ options.forEach((opt, i) => {
32
+ const mark = i + 1 === recommended ? ' ✅ 推荐' : '';
33
+ lines.push(` ${i + 1}. ${opt}${mark}`);
34
+ });
35
+ const timeoutMin = Math.round(timeoutMs / 60000);
36
+ lines.push(``, `⏱ ${timeoutMin}分钟无回复自动选择推荐项`);
37
+
38
+ const msg = lines.join('\n');
39
+ adapter.reply(threadId, msg).catch(() => {});
40
+
41
+ const targets = new Set([threadId, ...broadcastTo]);
42
+ for (const tid of targets) {
43
+ if (tid !== threadId) {
44
+ adapter.reply(tid, msg).catch(() => {});
45
+ }
46
+ }
47
+
48
+ const timer = setTimeout(() => {
49
+ if (pendingDecisions.has(threadId)) {
50
+ pendingDecisions.delete(threadId);
51
+ const choice = recommended;
52
+ const timeoutMsg = `⏰ 超时,自动选择推荐项: ${options[choice - 1]}`;
53
+ adapter.reply(threadId, timeoutMsg).catch(() => {});
54
+ for (const tid of targets) {
55
+ if (tid !== threadId) {
56
+ adapter.reply(tid, timeoutMsg).catch(() => {});
57
+ }
58
+ }
59
+ resolve(choice);
60
+ }
61
+ }, timeoutMs);
62
+
63
+ pendingDecisions.set(threadId, { resolve, timer, options, recommended });
64
+ });
65
+ }
66
+
67
+ export function cancelDecision(threadId, reason = '') {
68
+ const pd = pendingDecisions.get(threadId);
69
+ if (!pd) return;
70
+ clearTimeout(pd.timer);
71
+ pendingDecisions.delete(threadId);
72
+ pd.resolve(0);
73
+ }
@@ -0,0 +1,141 @@
1
+ import { createSession, sendMessage } from '../opencode/client.js';
2
+ import { sendDecision, cancelDecision } from './decisions.js';
3
+
4
+ let autoLoopAbort = null;
5
+ let autoContext = null;
6
+
7
+ export function isAutoRunning() {
8
+ return autoLoopAbort !== null && !autoLoopAbort.signal.aborted;
9
+ }
10
+
11
+ export function stopAutoLoop() {
12
+ if (autoLoopAbort) {
13
+ autoLoopAbort.abort();
14
+ autoLoopAbort = null;
15
+ }
16
+ autoContext = null;
17
+ }
18
+
19
+ export async function startAutoLoop({ adapter, threadId, goal, openCodeSessions, broadcastTo = [] }) {
20
+ stopAutoLoop();
21
+ autoLoopAbort = new AbortController();
22
+ const signal = autoLoopAbort.signal;
23
+ autoContext = { adapter, threadId, goal, openCodeSessions, broadcastTo };
24
+
25
+ const session = await createSession(`auto-${Date.now()}`, `自主研发: ${goal.slice(0, 40)}`);
26
+ if (!session) {
27
+ await adapter.reply(threadId, '❌ 无法创建自主研发会话');
28
+ return;
29
+ }
30
+ openCodeSessions.set(threadId, session);
31
+
32
+ const sysPrompt = `当前项目: 遥控器 (opencode-remote-control)
33
+ 目标: ${goal}
34
+
35
+ 你是自主开发模式,可以读取项目代码、修改文件、运行命令。
36
+ 当遇到需要人类决定的选项时,在回复中包含以下格式:
37
+
38
+ [DECISION]
39
+ 问题描述
40
+ 1. 选项一
41
+ 2. 选项二
42
+ 推荐: 1
43
+ [DECISION]
44
+
45
+ 除此之外,请自主推进工作。完成后输出: ✅ 任务完成: 简要总结`;
46
+
47
+ const taskRunner = async () => {
48
+ try {
49
+ await adapter.reply(threadId, `🤖 开始自主开发: ${goal}`);
50
+ let fullText = '';
51
+ await sendMessage(session, sysPrompt, {
52
+ onResponseMeta: (meta) => {
53
+ if (meta.providerID && meta.modelID) {
54
+ console.log(`[auto] using ${meta.providerID}/${meta.modelID}`);
55
+ }
56
+ },
57
+ onText: (text) => {
58
+ fullText += text;
59
+ const decisionMatch = fullText.match(/\[DECISION\]([\s\S]*?)\[\/DECISION\]/);
60
+ if (decisionMatch) {
61
+ throw new DecisionRequired(decisionMatch[1].trim());
62
+ }
63
+ },
64
+ }, threadId);
65
+
66
+ await adapter.reply(threadId, `✅ 自主开发完成`);
67
+ } catch (e) {
68
+ if (e.name === 'DecisionRequired') {
69
+ await handleDecision(e.message);
70
+ } else if (!signal.aborted) {
71
+ console.error('[auto] error:', e.message);
72
+ await adapter.reply(threadId, `❌ 自主开发出错: ${e.message}`);
73
+ }
74
+ } finally {
75
+ if (!signal.aborted) {
76
+ autoLoopAbort = null;
77
+ autoContext = null;
78
+ }
79
+ }
80
+ };
81
+
82
+ taskRunner();
83
+ }
84
+
85
+ class DecisionRequired extends Error {
86
+ constructor(text) { super(text); this.name = 'DecisionRequired'; }
87
+ }
88
+
89
+ async function handleDecision(decisionText) {
90
+ const ctx = autoContext;
91
+ if (!ctx || ctx.signal?.aborted) return;
92
+ const { adapter, threadId, broadcastTo } = ctx;
93
+
94
+ const lines = decisionText.split('\n').map(l => l.trim()).filter(Boolean);
95
+ const question = lines[0] || '请选择';
96
+ const options = [];
97
+ let recommended = 1;
98
+ for (const l of lines) {
99
+ const optMatch = l.match(/^(\d+)\.\s*(.+)/);
100
+ if (optMatch) {
101
+ options.push(optMatch[2]);
102
+ if (optMatch[1]) recommended = parseInt(optMatch[1], 10);
103
+ }
104
+ const recMatch = l.match(/推荐:\s*(\d+)/);
105
+ if (recMatch) recommended = parseInt(recMatch[1], 10);
106
+ }
107
+ if (options.length === 0) {
108
+ options.push('继续');
109
+ }
110
+
111
+ const choice = await sendDecision({ adapter, threadId, question, options, recommended, timeoutMs: 5 * 60 * 1000, broadcastTo });
112
+ if (ctx.signal?.aborted) return;
113
+
114
+ if (choice === 0) {
115
+ await adapter.reply(threadId, '⏹ 决策取消,自主开发停止');
116
+ return;
117
+ }
118
+
119
+ const chosen = options[choice - 1];
120
+ const userChoice = `选择了: ${choice}. ${chosen}`;
121
+
122
+ const { session } = ctx;
123
+ sendMessage(session, userChoice, {
124
+ onText: (text) => {
125
+ const decisionMatch = text.match(/\[DECISION\]([\s\S]*?)\[\/DECISION\]/);
126
+ if (decisionMatch) {
127
+ throw new DecisionRequired(decisionMatch[1].trim());
128
+ }
129
+ },
130
+ }, threadId).then(async (result) => {
131
+ if (result && result.includes('✅')) {
132
+ await adapter.reply(threadId, `✅ 自主开发完成`);
133
+ }
134
+ }).catch(async (e) => {
135
+ if (e.name === 'DecisionRequired') {
136
+ await handleDecision(e.message);
137
+ } else if (!ctx.signal?.aborted) {
138
+ console.error('[auto] post-decision error:', e.message);
139
+ }
140
+ });
141
+ }
package/dist/cli.js CHANGED
@@ -47,17 +47,9 @@ Multi-Bot Support:
47
47
  Weixin Bot Commands (send in WeChat):
48
48
  /start — Claim ownership
49
49
  /help — Show all commands
50
- /status — Check connection
51
- /stop — Interrupt task
52
50
  /reset — Reset session
53
51
  /restart — Restart bot
54
- /sessionsBrowse sessions
55
- /delsessions — Delete sessions
56
- /loop — Loop task
57
- /refresh — Refresh context
58
- /copy — Copy latest reply
59
- /revert — Undo last message
60
- /upload — Upload build artifacts
52
+ /diagnoseSystem diagnostics
61
53
  /model — Switch AI model
62
54
 
63
55
  Multi-Agent Commands:
@@ -65,7 +57,6 @@ Multi-Agent Commands:
65
57
  /cc <prompt> — Use Claude Code
66
58
  /cx <prompt> — Use Codex
67
59
  /copilot <prompt> — Use GitHub Copilot
68
- /agents — List all available agents
69
60
 
70
61
  Examples:
71
62
  opencode-remote # Start all bots
@@ -26,6 +26,7 @@ export function loadConfig() {
26
26
 
27
27
  const appSecret = content.match(/FEISHU_APP_SECRET=(.+)/)?.[1]?.trim();
28
28
  if (appSecret) config.feishuAppSecret = appSecret;
29
+
29
30
  }
30
31
 
31
32
  // 2. Read ./.env (local project, lower priority)
@@ -56,6 +57,5 @@ export function loadConfig() {
56
57
  if (process.env.SESSION_IDLE_TIMEOUT_MS) config.sessionIdleTimeoutMs = parseInt(process.env.SESSION_IDLE_TIMEOUT_MS, 10);
57
58
  if (process.env.CLEANUP_INTERVAL_MS) config.cleanupIntervalMs = parseInt(process.env.CLEANUP_INTERVAL_MS, 10);
58
59
  if (process.env.APPROVAL_TIMEOUT_MS) config.approvalTimeoutMs = parseInt(process.env.APPROVAL_TIMEOUT_MS, 10);
59
-
60
60
  return config;
61
61
  }
@@ -0,0 +1,120 @@
1
+ import { execSync } from 'child_process';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+
6
+ const DEFAULT_MIRRORS = [
7
+ 'gh-proxy.com',
8
+ 'ghfast.top',
9
+ 'mirror.ghproxy.com',
10
+ 'ghproxy.com',
11
+ 'github.akams.cn',
12
+ 'gh-proxy.ygxz.in',
13
+ 'codeload.github.com',
14
+ 'github.com',
15
+ ];
16
+
17
+ function loadCustomMirrors() {
18
+ const localMirrors = join(process.cwd(), '.gitmirrors');
19
+ if (existsSync(localMirrors)) {
20
+ return readFileSync(localMirrors, 'utf-8').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
21
+ }
22
+ const globalEnv = join(homedir(), '.opencode-remote', '.env');
23
+ if (existsSync(globalEnv)) {
24
+ const m = readFileSync(globalEnv, 'utf-8').match(/GIT_MIRRORS=(.+)/);
25
+ if (m) return m[1].split(',').map(s => s.trim()).filter(Boolean);
26
+ }
27
+ if (process.env.GIT_MIRRORS) return process.env.GIT_MIRRORS.split(',').map(s => s.trim()).filter(Boolean);
28
+ return DEFAULT_MIRRORS;
29
+ }
30
+
31
+ function parseOriginUrl(url) {
32
+ // HTTPS: https://TOKEN@github.com/user/repo.git
33
+ // https://github.com/user/repo.git
34
+ // SSH: git@github.com:user/repo.git
35
+ let auth = '';
36
+ let host = '';
37
+ let userRepo = '';
38
+
39
+ if (url.includes('://')) {
40
+ const match = url.match(/^(https?:\/\/)([^@]*@)?([^\/]+)(\/.*)$/);
41
+ if (!match) return null;
42
+ const protocol = match[1];
43
+ const token = match[2] || '';
44
+ host = match[3];
45
+ userRepo = match[4];
46
+ auth = protocol + token;
47
+ } else {
48
+ const match = url.match(/^([^@]+@)?([^:]+):(.+)$/);
49
+ if (!match) return null;
50
+ auth = match[1] || '';
51
+ host = match[2];
52
+ userRepo = '/' + match[3];
53
+ }
54
+
55
+ return { auth, host, userRepo };
56
+ }
57
+
58
+ export function gitPush({ message, branch } = {}) {
59
+ const cwd = process.cwd();
60
+
61
+ let currentBranch;
62
+ try {
63
+ currentBranch = execSync('git branch --show-current', { cwd, encoding: 'utf-8' }).trim();
64
+ } catch (e) {
65
+ return { ok: false, error: '不在 git 仓库中' };
66
+ }
67
+ const targetBranch = branch || currentBranch;
68
+
69
+ const status = execSync('git status --porcelain', { cwd, encoding: 'utf-8' }).trim();
70
+ if (status) {
71
+ execSync('git add -A', { cwd });
72
+ const msg = (message || `auto update ${new Date().toISOString().slice(0, 19)}`).replace(/"/g, '\\"');
73
+ execSync(`git commit -m "${msg}"`, { cwd, stdio: 'pipe' });
74
+ }
75
+
76
+ let originUrl;
77
+ try {
78
+ originUrl = execSync('git remote get-url origin', { cwd, encoding: 'utf-8' }).trim();
79
+ } catch (e) {
80
+ return { ok: false, error: '没有找到 remote origin' };
81
+ }
82
+
83
+ const parsed = parseOriginUrl(originUrl);
84
+ if (!parsed) {
85
+ return { ok: false, error: `无法解析 remote URL: ${originUrl}` };
86
+ }
87
+
88
+ // 从 gh CLI 提取 token(如果 remote URL 里没有的话)
89
+ let pushAuth = parsed.auth;
90
+ if (originUrl.startsWith('https://') && !originUrl.includes('@')) {
91
+ try {
92
+ const ghToken = execSync('gh auth token', { encoding: 'utf-8' }).trim();
93
+ if (ghToken) {
94
+ pushAuth = `https://${ghToken}@`;
95
+ }
96
+ } catch (_) { /* gh not available */ }
97
+ }
98
+
99
+ const mirrors = loadCustomMirrors();
100
+ const results = [];
101
+
102
+ for (const host of mirrors) {
103
+ const pushUrl = pushAuth + host + parsed.userRepo;
104
+ const remoteName = `m_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
105
+ console.log(`[git-push] trying ${host}...`);
106
+ try {
107
+ execSync(`git remote add ${remoteName} "${pushUrl}"`, { cwd, stdio: 'pipe' });
108
+ execSync(`git push ${remoteName} ${targetBranch} --follow-tags`, { cwd, stdio: 'pipe', timeout: 30000 });
109
+ execSync(`git remote remove ${remoteName}`, { cwd, stdio: 'pipe' });
110
+ results.push({ host, ok: true });
111
+ return { ok: true, results, successUrl: pushUrl };
112
+ } catch (e) {
113
+ try { execSync(`git remote remove ${remoteName}`, { cwd, stdio: 'pipe' }); } catch (_) {}
114
+ const msg = e.stderr?.toString()?.trim() || e.message || '';
115
+ results.push({ host, ok: false, error: msg.slice(0, 150) });
116
+ }
117
+ }
118
+
119
+ return { ok: false, results };
120
+ }
@@ -53,7 +53,7 @@ export const TEMPLATES = {
53
53
  botStarted: () => formatNotification({
54
54
  type: 'started',
55
55
  title: 'OpenCode Remote Control ready',
56
- actions: ['💬 Send a prompt to start', '/help — commands', '/statusconnection']
56
+ actions: ['💬 Send a prompt to start', '/help — commands', '/diagnosediagnostics']
57
57
  }),
58
58
  sessionExpired: () => formatNotification({
59
59
  type: 'expired',
@@ -81,7 +81,7 @@ export const TEMPLATES = {
81
81
  type: 'error',
82
82
  title: 'OpenCode is offline',
83
83
  details: 'Cannot connect to OpenCode server.',
84
- actions: ['🔄 /retry — check again', '/status — diagnostics']
84
+ actions: ['🔄 /retry — check again', '/diagnose — diagnostics']
85
85
  }),
86
86
  thinking: () => formatNotification({
87
87
  type: 'loading',