Compare commits

...

8 Commits

Author SHA1 Message Date
jiantw83 12d7403a0e chore(ai-review 狀態): 移除已解決的 OpenCode TLS finding
AI / 計算版本號 (pull_request) Successful in 2s
AI / Code Review (pull_request) Successful in 29s
2026-06-20 13:42:25 +00:00
jiantw83 babe599977 refactor(OpenCode TLS): 共用 httpsAgent 建立邏輯 2026-06-20 13:42:25 +00:00
admin c662a2e362 Merge pull request '新增 OpenCode 自簽憑證略過設定' (#20) from ai-review-resolve/20260620131600 into develop
Reviewed-on: #20
2026-06-20 13:18:16 +00:00
AI Review Bot 7a79cd45be chore: update ai-review findings [ai-review-bot][success]
AI / 計算版本號 (pull_request) Successful in 2s
AI / Code Review (pull_request) Successful in 5s
2026-06-20 13:17:36 +00:00
jiantw83 46a4ff91ce docs(README): 補充 OpenCode 自簽憑證設定
AI / 計算版本號 (pull_request) Successful in 3s
AI / Code Review (pull_request) Successful in 57s
2026-06-20 13:16:00 +00:00
jiantw83 82d2a943ac test(OpenCode TLS): 覆蓋自簽憑證略過設定 2026-06-20 13:16:00 +00:00
jiantw83 f7e4f09d4e feat(OpenCode TLS): 新增自簽憑證驗證略過設定 2026-06-20 13:16:00 +00:00
jiantw83 c7e63c9468 Merge pull request 'fix(ai-review 同步): 限制自動提交只包含問題檔' (#18) from ai-review-resolve/20260618080715 into develop
Reviewed-on: #18
2026-06-18 08:10:05 +00:00
7 changed files with 69 additions and 5 deletions
+2
View File
@@ -224,6 +224,8 @@ jobs:
OPENCODE_BASE_URL: http://192.168.3.124:4096
OPENCODE_PROVIDER: google
OPENCODE_MODEL: gemini-2.5-flash
# 若 OpenCode server 使用自簽憑證,才需要提供:
# OPENCODE_SKIP_TLS_VERIFY: true
# 若 OpenCode server 有設定 OPENCODE_SERVER_PASSWORD,才需要提供:
# OPENCODE_SERVER_USERNAME: opencode
# OPENCODE_SERVER_PASSWORD: ${{ secrets.OPENCODE_SERVER_PASSWORD }}
+5
View File
@@ -95,6 +95,10 @@ inputs:
OPENCODE_SERVER_PASSWORD:
description: 'OpenCode server Basic Auth password'
required: false
OPENCODE_SKIP_TLS_VERIFY:
description: '跳過 OpenCode server SSL/TLS 憑證驗證(自簽憑證時使用)'
required: false
default: 'false'
runs:
using: 'docker'
@@ -129,3 +133,4 @@ runs:
OPENCODE_PROVIDER: ${{ inputs.OPENCODE_PROVIDER }}
OPENCODE_SERVER_USERNAME: ${{ inputs.OPENCODE_SERVER_USERNAME }}
OPENCODE_SERVER_PASSWORD: ${{ inputs.OPENCODE_SERVER_PASSWORD }}
OPENCODE_SKIP_TLS_VERIFY: ${{ inputs.OPENCODE_SKIP_TLS_VERIFY }}
+10
View File
@@ -1,3 +1,5 @@
import https from 'https';
export const GITEA_TOKEN = process.env.GITEA_TOKEN || '';
export const GITEA_COMMENT_TOKEN = process.env.GITEA_COMMENT_TOKEN || '';
export const GITEA_SERVER_URL = process.env.GITEA_SERVER_URL || 'https://gitea.com';
@@ -11,6 +13,14 @@ export const PR_BASE_BRANCH = process.env.PR_BASE_BRANCH || '';
export const FINDINGS_PATH = '.gitea/ai-review/findings.json';
export const EXCLUSIONS_PATH = '.gitea/ai-review/exclusions.json';
export function shouldSkipOpenCodeTLSVerify() {
return process.env.OPENCODE_SKIP_TLS_VERIFY === 'true';
}
export function getOpenCodeHttpsAgent() {
return shouldSkipOpenCodeTLSVerify() ? new https.Agent({ rejectUnauthorized: false }) : undefined;
}
/** 將逗號分隔的 API key 字串拆成陣列 */
function splitKeys(value) {
if (!value) return [];
+10 -3
View File
@@ -1,5 +1,5 @@
import axios from 'axios';
import { getLLMConfig } from './config.js';
import { getLLMConfig, getOpenCodeHttpsAgent } from './config.js';
import { line, error } from './log.js';
function isOpenAIGpt55(provider, model) {
@@ -46,6 +46,13 @@ function applyOpenCodeAuth(headers) {
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
}
function opencodeAxiosOptions(headers) {
return {
headers,
httpsAgent: getOpenCodeHttpsAgent(),
};
}
function extractOpenCodeContent(data) {
const parts = data.parts || data.data?.parts || data.info?.content || data.data?.info?.content || [];
return parts
@@ -60,7 +67,7 @@ async function chatOpenCode(baseURL, model, systemPrompt, userContent, headers)
const session = await axios.post(
`${base}/session`,
{ title: 'AI Code Review', model: { providerID, id: modelID } },
{ headers }
opencodeAxiosOptions(headers)
);
const sessionID = session.data.id || session.data.data?.id;
if (!sessionID) throw new Error('OpenCode session 建立失敗:回應中沒有 session id');
@@ -72,7 +79,7 @@ async function chatOpenCode(baseURL, model, systemPrompt, userContent, headers)
system: systemPrompt,
parts: [{ type: 'text', text: userContent }],
},
{ headers }
opencodeAxiosOptions(headers)
);
return extractOpenCodeContent(resp.data);
}
+16
View File
@@ -12,6 +12,7 @@ const ENV_KEYS = [
'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL',
'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER',
'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD',
'OPENCODE_SKIP_TLS_VERIFY',
];
let saved = {};
@@ -163,6 +164,21 @@ describe('chat - key rotation', async () => {
assert.equal(headers[0]['Authorization'], `Basic ${Buffer.from('opencode:secret').toString('base64')}`);
});
it('passes an insecure https agent to OpenCode when TLS verification is disabled', async () => {
process.env.OPENCODE_BASE_URL = 'https://opencode.local:4096';
process.env.OPENCODE_SKIP_TLS_VERIFY = 'true';
const agents = [];
mock.method(axios, 'post', async (url, _payload, opts) => {
agents.push(opts.httpsAgent);
if (url.endsWith('/session')) return { data: { id: 'ses_test' } };
return { data: { parts: [{ type: 'text', text: 'ok' }] } };
});
await chat('sys', 'user');
assert.equal(agents.length, 2);
assert.equal(agents[0].options.rejectUnauthorized, false);
assert.equal(agents[1].options.rejectUnauthorized, false);
});
it('uses Responses API for openai GPT-5.5', async () => {
process.env.OPENAI_API_KEY = 'sk-test';
process.env.OPENAI_MODEL = 'GPT-5.5';
+8 -2
View File
@@ -7,6 +7,7 @@ import {
GITEA_REPOSITORY,
GITEA_SKIP_TLS_VERIFY,
PR_NUMBER,
getOpenCodeHttpsAgent,
getLLMConfig,
} from './config.js';
import { verifyRemoteAccess } from './git.js';
@@ -26,6 +27,11 @@ const applyOpenCodeAuth = (headers) => {
const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode';
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
};
const opencodeAxiosOptions = (headers) => ({
headers,
timeout: 30000,
httpsAgent: getOpenCodeHttpsAgent(),
});
function giteaErr(e) {
const status = e.response?.status;
@@ -89,8 +95,8 @@ export async function verifyLLM() {
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 });
await axios.get(`${base}/global/health`, opencodeAxiosOptions(headers));
const providers = await axios.get(`${base}/config/providers`, opencodeAxiosOptions(headers));
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}` };
+18
View File
@@ -11,6 +11,7 @@ const LLM_ENV_KEYS = [
'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL',
'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER',
'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD',
'OPENCODE_SKIP_TLS_VERIFY',
];
function clearLLMEnv() {
@@ -182,6 +183,23 @@ describe('verifyLLM', () => {
assert.deepEqual(urls, ['http://opencode.local:4096/global/health', 'http://opencode.local:4096/config/providers']);
});
it('passes an insecure https agent for opencode when TLS verification is disabled', async () => {
clearLLMEnv();
process.env.OPENCODE_BASE_URL = 'https://opencode.local:4096';
process.env.OPENCODE_SKIP_TLS_VERIFY = 'true';
const agents = [];
mock.method(axios, 'get', async (url, opts) => {
agents.push(opts.httpsAgent);
if (url.endsWith('/global/health')) return { data: { healthy: true } };
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(agents.length, 2);
assert.equal(agents[0].options.rejectUnauthorized, false);
assert.equal(agents[1].options.rejectUnauthorized, false);
});
it('checks openai GPT-5.5 with Responses API', async () => {
clearLLMEnv();
process.env.OPENAI_API_KEY = 'sk-test';