Merge pull request '新增 OpenCode 自簽憑證略過設定' (#21) from develop into master
Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
@@ -1 +1,9 @@
|
|||||||
[]
|
[
|
||||||
|
{
|
||||||
|
"level": "warning",
|
||||||
|
"role": "Mage",
|
||||||
|
"location": "app/preflight.js:23",
|
||||||
|
"suggestion": "在 `app/llm.js` 與 `app/preflight.js` 中,`opencodeAxiosOptions` 函數的邏輯存在重複。雖然 `timeout` 參數在兩處有所不同,但處理 `httpsAgent` 的核心邏輯是相同的。建議將 `httpsAgent` 的建立邏輯抽象為一個共用函數或在 `config.js` 中定義,以避免未來修改時造成不一致,並提高程式碼的可維護性。\n\n例如,可以將 `httpsAgent` 的邏輯移至 `config.js`:\n```javascript\n// app/config.js\nexport function getOpenCodeHttpsAgent() {\n return shouldSkipOpenCodeTLSVerify() ? new https.Agent({ rejectUnauthorized: false }) : undefined;\n}\n\n// app/llm.js\nimport { getOpenCodeHttpsAgent } from './config.js';\nfunction opencodeAxiosOptions(headers) {\n return {\n headers,\n httpsAgent: getOpenCodeHttpsAgent(),\n };\n}\n\n// app/preflight.js\nimport { getOpenCodeHttpsAgent } from './config.js';\nconst opencodeAxiosOptions = (headers) => ({\n headers,\n timeout: 30000,\n httpsAgent: getOpenCodeHttpsAgent(),\n});\n```",
|
||||||
|
"is_new": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ jobs:
|
|||||||
OPENCODE_BASE_URL: http://192.168.3.124:4096
|
OPENCODE_BASE_URL: http://192.168.3.124:4096
|
||||||
OPENCODE_PROVIDER: google
|
OPENCODE_PROVIDER: google
|
||||||
OPENCODE_MODEL: gemini-2.5-flash
|
OPENCODE_MODEL: gemini-2.5-flash
|
||||||
|
# 若 OpenCode server 使用自簽憑證,才需要提供:
|
||||||
|
# OPENCODE_SKIP_TLS_VERIFY: true
|
||||||
# 若 OpenCode server 有設定 OPENCODE_SERVER_PASSWORD,才需要提供:
|
# 若 OpenCode server 有設定 OPENCODE_SERVER_PASSWORD,才需要提供:
|
||||||
# OPENCODE_SERVER_USERNAME: opencode
|
# OPENCODE_SERVER_USERNAME: opencode
|
||||||
# OPENCODE_SERVER_PASSWORD: ${{ secrets.OPENCODE_SERVER_PASSWORD }}
|
# OPENCODE_SERVER_PASSWORD: ${{ secrets.OPENCODE_SERVER_PASSWORD }}
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ inputs:
|
|||||||
OPENCODE_SERVER_PASSWORD:
|
OPENCODE_SERVER_PASSWORD:
|
||||||
description: 'OpenCode server Basic Auth password'
|
description: 'OpenCode server Basic Auth password'
|
||||||
required: false
|
required: false
|
||||||
|
OPENCODE_SKIP_TLS_VERIFY:
|
||||||
|
description: '跳過 OpenCode server SSL/TLS 憑證驗證(自簽憑證時使用)'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: 'docker'
|
using: 'docker'
|
||||||
@@ -129,3 +133,4 @@ runs:
|
|||||||
OPENCODE_PROVIDER: ${{ inputs.OPENCODE_PROVIDER }}
|
OPENCODE_PROVIDER: ${{ inputs.OPENCODE_PROVIDER }}
|
||||||
OPENCODE_SERVER_USERNAME: ${{ inputs.OPENCODE_SERVER_USERNAME }}
|
OPENCODE_SERVER_USERNAME: ${{ inputs.OPENCODE_SERVER_USERNAME }}
|
||||||
OPENCODE_SERVER_PASSWORD: ${{ inputs.OPENCODE_SERVER_PASSWORD }}
|
OPENCODE_SERVER_PASSWORD: ${{ inputs.OPENCODE_SERVER_PASSWORD }}
|
||||||
|
OPENCODE_SKIP_TLS_VERIFY: ${{ inputs.OPENCODE_SKIP_TLS_VERIFY }}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ export const PR_BASE_BRANCH = process.env.PR_BASE_BRANCH || '';
|
|||||||
export const FINDINGS_PATH = '.gitea/ai-review/findings.json';
|
export const FINDINGS_PATH = '.gitea/ai-review/findings.json';
|
||||||
export const EXCLUSIONS_PATH = '.gitea/ai-review/exclusions.json';
|
export const EXCLUSIONS_PATH = '.gitea/ai-review/exclusions.json';
|
||||||
|
|
||||||
|
export function shouldSkipOpenCodeTLSVerify() {
|
||||||
|
return process.env.OPENCODE_SKIP_TLS_VERIFY === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
/** 將逗號分隔的 API key 字串拆成陣列 */
|
/** 將逗號分隔的 API key 字串拆成陣列 */
|
||||||
function splitKeys(value) {
|
function splitKeys(value) {
|
||||||
if (!value) return [];
|
if (!value) return [];
|
||||||
|
|||||||
+11
-3
@@ -1,5 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { getLLMConfig } from './config.js';
|
import https from 'https';
|
||||||
|
import { getLLMConfig, shouldSkipOpenCodeTLSVerify } from './config.js';
|
||||||
import { line, error } from './log.js';
|
import { line, error } from './log.js';
|
||||||
|
|
||||||
function isOpenAIGpt55(provider, model) {
|
function isOpenAIGpt55(provider, model) {
|
||||||
@@ -46,6 +47,13 @@ function applyOpenCodeAuth(headers) {
|
|||||||
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function opencodeAxiosOptions(headers) {
|
||||||
|
return {
|
||||||
|
headers,
|
||||||
|
httpsAgent: shouldSkipOpenCodeTLSVerify() ? new https.Agent({ rejectUnauthorized: false }) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function extractOpenCodeContent(data) {
|
function extractOpenCodeContent(data) {
|
||||||
const parts = data.parts || data.data?.parts || data.info?.content || data.data?.info?.content || [];
|
const parts = data.parts || data.data?.parts || data.info?.content || data.data?.info?.content || [];
|
||||||
return parts
|
return parts
|
||||||
@@ -60,7 +68,7 @@ async function chatOpenCode(baseURL, model, systemPrompt, userContent, headers)
|
|||||||
const session = await axios.post(
|
const session = await axios.post(
|
||||||
`${base}/session`,
|
`${base}/session`,
|
||||||
{ title: 'AI Code Review', model: { providerID, id: modelID } },
|
{ title: 'AI Code Review', model: { providerID, id: modelID } },
|
||||||
{ headers }
|
opencodeAxiosOptions(headers)
|
||||||
);
|
);
|
||||||
const sessionID = session.data.id || session.data.data?.id;
|
const sessionID = session.data.id || session.data.data?.id;
|
||||||
if (!sessionID) throw new Error('OpenCode session 建立失敗:回應中沒有 session id');
|
if (!sessionID) throw new Error('OpenCode session 建立失敗:回應中沒有 session id');
|
||||||
@@ -72,7 +80,7 @@ async function chatOpenCode(baseURL, model, systemPrompt, userContent, headers)
|
|||||||
system: systemPrompt,
|
system: systemPrompt,
|
||||||
parts: [{ type: 'text', text: userContent }],
|
parts: [{ type: 'text', text: userContent }],
|
||||||
},
|
},
|
||||||
{ headers }
|
opencodeAxiosOptions(headers)
|
||||||
);
|
);
|
||||||
return extractOpenCodeContent(resp.data);
|
return extractOpenCodeContent(resp.data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const ENV_KEYS = [
|
|||||||
'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL',
|
'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL',
|
||||||
'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER',
|
'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER',
|
||||||
'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD',
|
'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD',
|
||||||
|
'OPENCODE_SKIP_TLS_VERIFY',
|
||||||
];
|
];
|
||||||
|
|
||||||
let saved = {};
|
let saved = {};
|
||||||
@@ -163,6 +164,21 @@ describe('chat - key rotation', async () => {
|
|||||||
assert.equal(headers[0]['Authorization'], `Basic ${Buffer.from('opencode:secret').toString('base64')}`);
|
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 () => {
|
it('uses Responses API for openai GPT-5.5', async () => {
|
||||||
process.env.OPENAI_API_KEY = 'sk-test';
|
process.env.OPENAI_API_KEY = 'sk-test';
|
||||||
process.env.OPENAI_MODEL = 'GPT-5.5';
|
process.env.OPENAI_MODEL = 'GPT-5.5';
|
||||||
|
|||||||
+8
-2
@@ -8,6 +8,7 @@ import {
|
|||||||
GITEA_SKIP_TLS_VERIFY,
|
GITEA_SKIP_TLS_VERIFY,
|
||||||
PR_NUMBER,
|
PR_NUMBER,
|
||||||
getLLMConfig,
|
getLLMConfig,
|
||||||
|
shouldSkipOpenCodeTLSVerify,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import { verifyRemoteAccess } from './git.js';
|
import { verifyRemoteAccess } from './git.js';
|
||||||
import { step, line, ok, error } from './log.js';
|
import { step, line, ok, error } from './log.js';
|
||||||
@@ -26,6 +27,11 @@ const applyOpenCodeAuth = (headers) => {
|
|||||||
const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode';
|
const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode';
|
||||||
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||||
};
|
};
|
||||||
|
const opencodeAxiosOptions = (headers) => ({
|
||||||
|
headers,
|
||||||
|
timeout: 30000,
|
||||||
|
httpsAgent: shouldSkipOpenCodeTLSVerify() ? new https.Agent({ rejectUnauthorized: false }) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
function giteaErr(e) {
|
function giteaErr(e) {
|
||||||
const status = e.response?.status;
|
const status = e.response?.status;
|
||||||
@@ -89,8 +95,8 @@ export async function verifyLLM() {
|
|||||||
const { providerID, modelID } = opencodeModelConfig(model);
|
const { providerID, modelID } = opencodeModelConfig(model);
|
||||||
applyOpenCodeAuth(headers);
|
applyOpenCodeAuth(headers);
|
||||||
try {
|
try {
|
||||||
await axios.get(`${base}/global/health`, { headers, timeout: 30000 });
|
await axios.get(`${base}/global/health`, opencodeAxiosOptions(headers));
|
||||||
const providers = await axios.get(`${base}/config/providers`, { headers, timeout: 30000 });
|
const providers = await axios.get(`${base}/config/providers`, opencodeAxiosOptions(headers));
|
||||||
const configuredProvider = providers.data.providers?.find(p => p.id === providerID);
|
const configuredProvider = providers.data.providers?.find(p => p.id === providerID);
|
||||||
if (!configuredProvider) return { ok: false, provider, error: `OpenCode server 未設定 provider=${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}` };
|
if (!configuredProvider.models?.[modelID]) return { ok: false, provider, error: `OpenCode server provider=${providerID} 未列出 model=${modelID}` };
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const LLM_ENV_KEYS = [
|
|||||||
'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL',
|
'AMAZONQ_API_KEY', 'AMAZONQ_BASE_URL', 'AMAZONQ_MODEL',
|
||||||
'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER',
|
'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER',
|
||||||
'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD',
|
'OPENCODE_SERVER_USERNAME', 'OPENCODE_SERVER_PASSWORD',
|
||||||
|
'OPENCODE_SKIP_TLS_VERIFY',
|
||||||
];
|
];
|
||||||
|
|
||||||
function clearLLMEnv() {
|
function clearLLMEnv() {
|
||||||
@@ -182,6 +183,23 @@ describe('verifyLLM', () => {
|
|||||||
assert.deepEqual(urls, ['http://opencode.local:4096/global/health', 'http://opencode.local:4096/config/providers']);
|
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 () => {
|
it('checks openai GPT-5.5 with Responses API', async () => {
|
||||||
clearLLMEnv();
|
clearLLMEnv();
|
||||||
process.env.OPENAI_API_KEY = 'sk-test';
|
process.env.OPENAI_API_KEY = 'sk-test';
|
||||||
|
|||||||
Reference in New Issue
Block a user