From 98eaeb6050eb00f0b5496a690f14cae0525e69d3 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 17 Jun 2026 06:50:44 +0000 Subject: [PATCH 1/5] =?UTF-8?q?feat(opencode):=20=E6=96=B0=E5=A2=9E=20Open?= =?UTF-8?q?Code=20server=20provider=20=E4=B8=B2=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- action.yaml | 22 ++++++++++++ app/config.js | 1 + app/llm.js | 87 +++++++++++++++++++++++++++++++++++++++++++++--- app/preflight.js | 35 +++++++++++++++++-- 4 files changed, 138 insertions(+), 7 deletions(-) diff --git a/action.yaml b/action.yaml index 485776b..0d1e7cf 100644 --- a/action.yaml +++ b/action.yaml @@ -79,6 +79,23 @@ inputs: description: 'Amazon Q Base URL' required: false + # OpenCode Server + OPENCODE_BASE_URL: + description: 'OpenCode server Base URL' + required: false + OPENCODE_MODEL: + description: 'OpenCode model id' + required: false + OPENCODE_PROVIDER: + description: 'OpenCode server provider id' + required: false + OPENCODE_SERVER_USERNAME: + description: 'OpenCode server Basic Auth username' + required: false + OPENCODE_SERVER_PASSWORD: + description: 'OpenCode server Basic Auth password' + required: false + runs: using: 'docker' image: 'Dockerfile' @@ -107,3 +124,8 @@ runs: OLLAMA_MODEL: ${{ inputs.OLLAMA_MODEL }} AMAZONQ_API_KEY: ${{ inputs.AMAZONQ_API_KEY }} AMAZONQ_BASE_URL: ${{ inputs.AMAZONQ_BASE_URL }} + OPENCODE_BASE_URL: ${{ inputs.OPENCODE_BASE_URL }} + OPENCODE_MODEL: ${{ inputs.OPENCODE_MODEL }} + OPENCODE_PROVIDER: ${{ inputs.OPENCODE_PROVIDER }} + OPENCODE_SERVER_USERNAME: ${{ inputs.OPENCODE_SERVER_USERNAME }} + OPENCODE_SERVER_PASSWORD: ${{ inputs.OPENCODE_SERVER_PASSWORD }} diff --git a/app/config.js b/app/config.js index ee25071..e2e0c6f 100644 --- a/app/config.js +++ b/app/config.js @@ -24,6 +24,7 @@ export function getLLMConfig() { ['gemini', splitKeys(process.env.GEMINI_API_KEY), process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta', process.env.GEMINI_MODEL || 'gemini-2.5-flash'], ['ollama', ['ollama'], process.env.OLLAMA_BASE_URL, process.env.OLLAMA_MODEL], ['amazonq', splitKeys(process.env.AMAZONQ_API_KEY), process.env.AMAZONQ_BASE_URL || 'https://q.api.aws', process.env.AMAZONQ_MODEL || 'amazon-q'], + ['opencode', ['opencode'], process.env.OPENCODE_BASE_URL, process.env.OPENCODE_MODEL || 'gemini-2.5-flash'], ]; for (const [provider, apiKeys, baseURL, model] of checks) { if (apiKeys.length > 0 && baseURL) return { provider, apiKeys, baseURL, model }; diff --git a/app/llm.js b/app/llm.js index e1a401a..a686053 100644 --- a/app/llm.js +++ b/app/llm.js @@ -2,6 +2,81 @@ import axios from 'axios'; import { getLLMConfig } from './config.js'; import { line, error } from './log.js'; +function isOpenAIGpt55(provider, model) { + return provider === 'openai' && /^gpt-5\.5(?:-|$)/i.test(model || ''); +} + +function chatEndpoint(baseURL, provider, model) { + const base = baseURL.replace(/\/$/, ''); + return isOpenAIGpt55(provider, model) ? `${base}/responses` : `${base}/chat/completions`; +} + +function chatPayload(provider, model, systemPrompt, userContent) { + if (isOpenAIGpt55(provider, model)) { + return { model, instructions: systemPrompt, input: userContent, temperature: 0.2 }; + } + return { model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 }; +} + +function extractContent(provider, model, data) { + if (!isOpenAIGpt55(provider, model)) return data.choices[0].message.content; + if (typeof data.output_text === 'string') return data.output_text; + const parts = data.output?.flatMap(item => item.content || []) || []; + const text = parts + .map(part => { + if (typeof part.text === 'string') return part.text; + if (typeof part.content === 'string') return part.content; + return ''; + }) + .filter(Boolean) + .join(''); + if (text) return text; + return data.choices?.[0]?.message?.content || ''; +} + +function opencodeModelConfig(model) { + const [providerID, modelID] = model.includes('/') ? model.split('/', 2) : [process.env.OPENCODE_PROVIDER || 'google', model]; + return { providerID, modelID }; +} + +function applyOpenCodeAuth(headers) { + const password = process.env.OPENCODE_SERVER_PASSWORD; + if (!password) return; + const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode'; + headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; +} + +function extractOpenCodeContent(data) { + const parts = data.parts || data.data?.parts || data.info?.content || data.data?.info?.content || []; + return parts + .map(part => part.text || part.content || '') + .filter(Boolean) + .join(''); +} + +async function chatOpenCode(baseURL, model, systemPrompt, userContent, headers) { + const base = baseURL.replace(/\/$/, ''); + const { providerID, modelID } = opencodeModelConfig(model); + const session = await axios.post( + `${base}/session`, + { title: 'AI Code Review', model: { providerID, id: modelID } }, + { headers } + ); + const sessionID = session.data.id || session.data.data?.id; + if (!sessionID) throw new Error('OpenCode session 建立失敗:回應中沒有 session id'); + + const resp = await axios.post( + `${base}/session/${sessionID}/message`, + { + model: { providerID, modelID }, + system: systemPrompt, + parts: [{ type: 'text', text: userContent }], + }, + { headers } + ); + return extractOpenCodeContent(resp.data); +} + export async function chat(systemPrompt, userContent) { const { provider, apiKeys, baseURL, model } = getLLMConfig(); if (!provider) throw new Error('未設定任何 LLM API Key'); @@ -13,14 +88,18 @@ export async function chat(systemPrompt, userContent) { const shuffled = [...apiKeys].sort(() => Math.random() - 0.5); for (let i = 0; i < shuffled.length; i++) { - if (provider !== 'ollama') headers['Authorization'] = `Bearer ${shuffled[i]}`; + if (provider !== 'ollama' && provider !== 'opencode') headers['Authorization'] = `Bearer ${shuffled[i]}`; try { + if (provider === 'opencode') { + applyOpenCodeAuth(headers); + return await chatOpenCode(baseURL, model, systemPrompt, userContent, headers); + } const resp = await axios.post( - `${baseURL.replace(/\/$/, '')}/chat/completions`, - { model, messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], temperature: 0.2 }, + chatEndpoint(baseURL, provider, model), + chatPayload(provider, model, systemPrompt, userContent), { headers } ); - return resp.data.choices[0].message.content; + return extractContent(provider, model, resp.data); } catch (e) { line(`[LLM] key[${i + 1}/${shuffled.length}] 失敗: ${e.message}`); } diff --git a/app/preflight.js b/app/preflight.js index 781bdb3..bcaa394 100644 --- a/app/preflight.js +++ b/app/preflight.js @@ -15,6 +15,17 @@ import { step, line, ok, error } from './log.js'; const httpsAgent = GITEA_SKIP_TLS_VERIFY ? new https.Agent({ rejectUnauthorized: false }) : undefined; const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`; const giteaHeaders = (token) => ({ Authorization: `token ${token}`, 'Content-Type': 'application/json' }); +const usesResponsesApi = (provider, model) => provider === 'openai' && /^gpt-5\.5(?:-|$)/i.test(model || ''); +const opencodeModelConfig = (model) => { + const [providerID, modelID] = model.includes('/') ? model.split('/', 2) : [process.env.OPENCODE_PROVIDER || 'google', model]; + return { providerID, modelID }; +}; +const applyOpenCodeAuth = (headers) => { + const password = process.env.OPENCODE_SERVER_PASSWORD; + if (!password) return; + const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode'; + headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; +}; function giteaErr(e) { const status = e.response?.status; @@ -63,6 +74,7 @@ export async function verifyLLM() { if (!baseURL) return { ok: false, provider, error: `${provider} 缺少 base URL` }; const base = baseURL.replace(/\/$/, ''); + const headers = { 'Content-Type': 'application/json' }; if (provider === 'ollama') { try { @@ -73,14 +85,31 @@ export async function verifyLLM() { } } - const headers = { 'Content-Type': 'application/json' }; + if (provider === 'opencode') { + const { providerID, modelID } = opencodeModelConfig(model); + applyOpenCodeAuth(headers); + try { + await axios.get(`${base}/global/health`, { headers, timeout: 30000 }); + const providers = await axios.get(`${base}/config/providers`, { headers, timeout: 30000 }); + const configuredProvider = providers.data.providers?.find(p => p.id === providerID); + if (!configuredProvider) return { ok: false, provider, error: `OpenCode server 未設定 provider=${providerID}` }; + if (!configuredProvider.models?.[modelID]) return { ok: false, provider, error: `OpenCode server provider=${providerID} 未列出 model=${modelID}` }; + return { ok: true, provider }; + } catch (e) { + return { ok: false, provider, error: `OpenCode server 驗證失敗: ${e.message}` }; + } + } + if (provider === 'claude') headers['anthropic-version'] = '2023-06-01'; - const payload = { model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, temperature: 0 }; + const endpoint = usesResponsesApi(provider, model) ? `${base}/responses` : `${base}/chat/completions`; + const payload = usesResponsesApi(provider, model) + ? { model, input: 'ping', max_output_tokens: 1, temperature: 0 } + : { model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, temperature: 0 }; for (let i = 0; i < apiKeys.length; i++) { headers['Authorization'] = `Bearer ${apiKeys[i]}`; try { - await axios.post(`${base}/chat/completions`, payload, { headers, timeout: 30000 }); + await axios.post(endpoint, payload, { headers, timeout: 30000 }); return { ok: true, provider, keyIndex: i + 1, total: apiKeys.length }; } catch (e) { line(`[preflight] LLM key[${i + 1}/${apiKeys.length}] 驗證失敗: ${e.message}`); -- 2.53.0 From 3f78543159fe4e0851812a7cea0a48d5d45c535a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 17 Jun 2026 06:50:51 +0000 Subject: [PATCH 2/5] =?UTF-8?q?test(opencode):=20=E8=A3=9C=E4=B8=8A=20Open?= =?UTF-8?q?Code=20server=20provider=20=E6=B8=AC=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config.test.js | 20 ++++++++++++++ app/llm.test.js | 64 +++++++++++++++++++++++++++++++++++++++++++ app/preflight.test.js | 37 +++++++++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/app/config.test.js b/app/config.test.js index c648520..869e811 100644 --- a/app/config.test.js +++ b/app/config.test.js @@ -8,6 +8,8 @@ const ENV_KEYS = [ 'GEMINI_API_KEY', 'GEMINI_BASE_URL', 'GEMINI_MODEL', 'OLLAMA_BASE_URL', 'OLLAMA_MODEL', 'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL', + 'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER', + 'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD', ]; let saved = {}; @@ -84,6 +86,24 @@ describe('getLLMConfig', () => { assert.equal(cfg.model, 'my-amazon-model'); }); + it('detects opencode server with gemini defaults', () => { + process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'opencode'); + assert.deepEqual(cfg.apiKeys, ['opencode']); + assert.equal(cfg.baseURL, 'http://opencode.local:4096'); + assert.equal(cfg.model, 'gemini-2.5-flash'); + }); + + it('detects opencode server with custom model', () => { + process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; + process.env.OPENCODE_MODEL = 'google/gemini-2.5-pro'; + const cfg = getLLMConfig(); + assert.equal(cfg.provider, 'opencode'); + assert.equal(cfg.baseURL, 'http://opencode.local:4096'); + assert.equal(cfg.model, 'google/gemini-2.5-pro'); + }); + it('openai takes priority over gemini when both set', () => { process.env.OPENAI_API_KEY = 'sk-test'; process.env.GEMINI_API_KEY = 'gemini-key'; diff --git a/app/llm.test.js b/app/llm.test.js index c4654ab..be2756a 100644 --- a/app/llm.test.js +++ b/app/llm.test.js @@ -10,6 +10,8 @@ const ENV_KEYS = [ 'CLAUDE_API_KEY', 'CLAUDE_BASE_URL', 'CLAUDE_MODEL', 'OLLAMA_BASE_URL', 'OLLAMA_MODEL', 'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL', + 'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER', + 'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD', ]; let saved = {}; @@ -125,6 +127,68 @@ describe('chat - key rotation', async () => { await chat('sys', 'user'); assert.equal(capturedHeaders['anthropic-version'], '2023-06-01'); }); + + it('uses OpenCode server session API for opencode', async () => { + process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; + process.env.OPENCODE_PROVIDER = 'google'; + process.env.OPENCODE_MODEL = 'gemini-2.5-flash'; + const calls = []; + mock.method(axios, 'post', async (url, payload, opts) => { + calls.push({ url, payload, headers: opts.headers }); + if (url.endsWith('/session')) return { data: { id: 'ses_test' } }; + return { data: { parts: [{ type: 'text', text: 'opencode response' }] } }; + }); + const result = await chat('sys', 'user'); + assert.equal(result, 'opencode response'); + assert.equal(calls[0].url, 'http://opencode.local:4096/session'); + assert.deepEqual(calls[0].payload.model, { providerID: 'google', id: 'gemini-2.5-flash' }); + assert.equal(calls[1].url, 'http://opencode.local:4096/session/ses_test/message'); + assert.deepEqual(calls[1].payload.model, { providerID: 'google', modelID: 'gemini-2.5-flash' }); + assert.equal(calls[1].payload.system, 'sys'); + assert.deepEqual(calls[1].payload.parts, [{ type: 'text', text: 'user' }]); + assert.equal(calls[1].headers['Authorization'], undefined); + }); + + it('uses Basic Auth for protected OpenCode server', async () => { + process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; + process.env.OPENCODE_SERVER_USERNAME = 'opencode'; + process.env.OPENCODE_SERVER_PASSWORD = 'secret'; + const headers = []; + mock.method(axios, 'post', async (url, _payload, opts) => { + headers.push(opts.headers); + if (url.endsWith('/session')) return { data: { id: 'ses_test' } }; + return { data: { parts: [{ type: 'text', text: 'ok' }] } }; + }); + await chat('sys', 'user'); + assert.equal(headers[0]['Authorization'], `Basic ${Buffer.from('opencode:secret').toString('base64')}`); + }); + + it('uses Responses API for openai GPT-5.5', async () => { + process.env.OPENAI_API_KEY = 'sk-test'; + process.env.OPENAI_MODEL = 'GPT-5.5'; + let capturedUrl, capturedPayload; + mock.method(axios, 'post', async (url, payload) => { + capturedUrl = url; + capturedPayload = payload; + return { data: { output_text: 'gpt response' } }; + }); + const result = await chat('sys', 'user'); + assert.equal(result, 'gpt response'); + assert.equal(capturedUrl, 'https://api.openai.com/v1/responses'); + assert.deepEqual(capturedPayload, { model: 'GPT-5.5', instructions: 'sys', input: 'user', temperature: 0.2 }); + }); + + it('extracts opencode text from message parts', async () => { + process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; + let calls = 0; + mock.method(axios, 'post', async () => { + calls += 1; + if (calls === 1) return { data: { id: 'ses_test' } }; + return { data: { parts: [{ type: 'text', text: 'hello' }, { type: 'text', text: ' world' }] } }; + }); + const result = await chat('sys', 'user'); + assert.equal(result, 'hello world'); + }); }); describe('chatJSON', async () => { diff --git a/app/preflight.test.js b/app/preflight.test.js index d654bc2..7a49678 100644 --- a/app/preflight.test.js +++ b/app/preflight.test.js @@ -9,6 +9,8 @@ const LLM_ENV_KEYS = [ 'GEMINI_API_KEY', 'GEMINI_BASE_URL', 'GEMINI_MODEL', 'OLLAMA_BASE_URL', 'OLLAMA_MODEL', 'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL', + 'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER', + 'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD', ]; function clearLLMEnv() { @@ -163,6 +165,41 @@ describe('verifyLLM', () => { assert.equal(capturedHeaders['anthropic-version'], '2023-06-01'); }); + it('checks opencode server provider and model', async () => { + clearLLMEnv(); + process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; + process.env.OPENCODE_PROVIDER = 'google'; + process.env.OPENCODE_MODEL = 'gemini-2.5-flash'; + const urls = []; + mock.method(axios, 'get', async (url) => { + urls.push(url); + if (url.endsWith('/global/health')) return { data: { healthy: true, version: '1.17.7' } }; + return { data: { providers: [{ id: 'google', models: { 'gemini-2.5-flash': { id: 'gemini-2.5-flash' } } }] } }; + }); + const result = await verifyLLM(); + assert.equal(result.ok, true); + assert.equal(result.provider, 'opencode'); + assert.deepEqual(urls, ['http://opencode.local:4096/global/health', 'http://opencode.local:4096/config/providers']); + }); + + it('checks openai GPT-5.5 with Responses API', async () => { + clearLLMEnv(); + process.env.OPENAI_API_KEY = 'sk-test'; + process.env.OPENAI_MODEL = 'GPT-5.5'; + let capturedUrl, capturedPayload; + mock.method(axios, 'post', async (url, payload) => { + capturedUrl = url; + capturedPayload = payload; + return { data: { output_text: 'o' } }; + }); + const result = await verifyLLM(); + assert.equal(result.ok, true); + assert.equal(result.provider, 'openai'); + assert.equal(capturedUrl, 'https://api.openai.com/v1/responses'); + assert.equal(capturedPayload.model, 'GPT-5.5'); + assert.equal(capturedPayload.max_output_tokens, 1); + }); + it('checks base URL connectivity for ollama (no key)', async () => { clearLLMEnv(); process.env.OLLAMA_BASE_URL = 'http://ollama.local/v1'; -- 2.53.0 From a9ac7857be5a26247eca25a1e87bcf90f67109eb Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 17 Jun 2026 06:50:56 +0000 Subject: [PATCH 3/5] =?UTF-8?q?test(comments):=20=E6=94=B9=E5=96=84?= =?UTF-8?q?=E5=9A=B4=E9=87=8D=E5=95=8F=E9=A1=8C=E7=95=99=E8=A8=80=E6=96=B7?= =?UTF-8?q?=E8=A8=80=E5=8F=AF=E8=AE=80=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comments.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/comments.test.js b/app/comments.test.js index 9632545..78b0e0f 100644 --- a/app/comments.test.js +++ b/app/comments.test.js @@ -163,6 +163,7 @@ describe('postNewCriticalComments', () => { }); it('handles multiple criticals, posting inline where possible and degrading the rest', async () => { + const criticalCommentPattern = /嚴重問題/; const inlineCalls = []; const issueCalls = []; const findings = [ @@ -181,6 +182,6 @@ describe('postNewCriticalComments', () => { assert.equal(inlineCalls[0].path, 'app/a.js'); assert.equal(inlineCalls[0].line, 10); assert.equal(issueCalls.length, 2); - assert.ok(issueCalls.every(b => /嚴重問題/.test(b))); + assert.ok(issueCalls.every(b => criticalCommentPattern.test(b))); }); }); -- 2.53.0 From 1492f1b9b4bf70e79dcf4e847f6be307b68d5189 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 17 Jun 2026 06:51:03 +0000 Subject: [PATCH 4/5] =?UTF-8?q?docs(README):=20=E8=A3=9C=E4=B8=8A=20OpenCo?= =?UTF-8?q?de=20server=20=E8=A8=AD=E5=AE=9A=E8=AA=AA=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 82f5a68..0e48726 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,14 @@ jobs: issues: write ``` +OpenAI GPT-5.5 會透過 Responses API 呼叫;設定方式仍使用 `OPENAI_*`: + +```yaml + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: https://api.openai.com/v1 + OPENAI_MODEL: gpt-5.5 +``` + ### 2. OpenRouter ```yaml name: AI @@ -192,7 +200,54 @@ jobs: issues: write ``` -### 6. Ollama +### 6. OpenCode Server +```yaml +name: AI +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true +on: + pull_request: + branches-ignore: + - master + types: [opened, synchronize] +jobs: + code-review: + name: Code Review + runs-on: ubuntu + steps: + - name: AI Code Review + uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }} + with: + GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }} + GITEA_COMMENT_TOKEN: ${{ secrets.GITEA_TOKEN }} + OPENCODE_BASE_URL: http://192.168.3.124:4096 + OPENCODE_PROVIDER: google + OPENCODE_MODEL: gemini-2.5-flash + # 若 OpenCode server 有設定 OPENCODE_SERVER_PASSWORD,才需要提供: + # OPENCODE_SERVER_USERNAME: opencode + # OPENCODE_SERVER_PASSWORD: ${{ secrets.OPENCODE_SERVER_PASSWORD }} + permissions: + contents: write + pull-requests: write + issues: write +``` + +OpenCode Server 串接方式會呼叫 server root 的 `/session` 與 `/session/{sessionID}/message`,並把模型指定為 `providerID=google`、`modelID=gemini-2.5-flash`。可用的內部 OpenCode server: + +```yaml + OPENCODE_BASE_URL: https://opencode.jsc.idv.me +``` + +或: + +```yaml + OPENCODE_BASE_URL: http://192.168.3.124:4096 +``` + +OpenCode server 本身必須已設定好 `google` provider 與 `gemini-2.5-flash` model;此 action 不會把 Google API key 傳給 OpenCode server。 + +### 7. Ollama ```yaml name: AI -- 2.53.0 From 3bfdefa4baea365aceb72323f731d84b181c613f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 17 Jun 2026 06:51:08 +0000 Subject: [PATCH 5/5] =?UTF-8?q?chore(ai-review):=20=E6=B8=85=E7=A9=BA?= =?UTF-8?q?=E5=B7=B2=E8=99=95=E7=90=86=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/ai-review/findings.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index b426b3b..fe51488 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,9 +1 @@ -[ - { - "level": "warning", - "role": "Bard", - "location": "app/comments.test.js:172", - "suggestion": "此處斷言使用了魔術字串 `/嚴重問題/`,就像樂譜中突然出現的無標記音符,雖能理解,卻少了點優雅與明確。建議將此字串提取為一個具名常數,或至少賦予一個描述性變數,以提升可讀性與未來維護的便利性,讓意圖更加清晰。", - "is_new": true - } -] +[] -- 2.53.0