合并 master 并整合 Router 安全修复
合入 origin/master 最新提交 a465e481e
保留后台错误报告、AGC 受控搜索与 Router 计费安全修复
同步 SpacetimeDB migration、技能文档和配置契约
This commit is contained in:
@@ -23,6 +23,9 @@ import type {
|
||||
AdminEditorShowcaseListQuery,
|
||||
AdminEditorShowcaseListResponse,
|
||||
AdminEditorShowcaseReviewRequest,
|
||||
AdminErrorReportDetail,
|
||||
AdminErrorReportEntry,
|
||||
AdminErrorReportListResponse,
|
||||
AdminExternalApiKeyListQuery,
|
||||
AdminExternalApiKeyListResponse,
|
||||
AdminFeatureGateConfigResponse,
|
||||
@@ -271,7 +274,16 @@ export function listAdminTrackingEvents(
|
||||
query: AdminTrackingEventListQuery = {},
|
||||
) {
|
||||
return request<AdminTrackingEventListResponse>(
|
||||
`/admin/api/tracking/events${buildQueryString(query)}`,
|
||||
`/admin/api/tracking/events${buildQueryString((params) => {
|
||||
appendQueryParam(params, 'eventKey', query.eventKey);
|
||||
appendQueryParam(params, 'userId', query.userId);
|
||||
appendQueryParam(params, 'scopeKind', query.scopeKind);
|
||||
appendQueryParam(params, 'scopeId', query.scopeId);
|
||||
appendQueryParam(params, 'startDate', query.startDate);
|
||||
appendQueryParam(params, 'endDate', query.endDate);
|
||||
appendNumericQueryParam(params, 'limit', query.limit);
|
||||
if (query.exportAll) params.set('exportAll', 'true');
|
||||
})}`,
|
||||
{ token },
|
||||
);
|
||||
}
|
||||
@@ -283,6 +295,60 @@ export function listAdminTrackingEventKeys(token: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function listAdminErrorReports(
|
||||
token: string,
|
||||
query: {
|
||||
status?: string;
|
||||
fingerprint?: string;
|
||||
source?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} = {},
|
||||
) {
|
||||
return request<AdminErrorReportListResponse>(
|
||||
`/admin/api/error-reports${buildErrorReportQueryString(query)}`,
|
||||
{ token },
|
||||
);
|
||||
}
|
||||
|
||||
export function getAdminErrorReport(token: string, batchId: string) {
|
||||
return request<AdminErrorReportDetail>(
|
||||
`/admin/api/error-reports/${encodeURIComponent(batchId)}`,
|
||||
{ token },
|
||||
);
|
||||
}
|
||||
|
||||
export function updateAdminErrorReport(
|
||||
token: string,
|
||||
batchId: string,
|
||||
payload: { status: string; note?: string },
|
||||
) {
|
||||
return request<AdminErrorReportEntry>(
|
||||
`/admin/api/error-reports/${encodeURIComponent(batchId)}`,
|
||||
{ method: 'PATCH', token, body: payload },
|
||||
);
|
||||
}
|
||||
|
||||
export function getAdminErrorReportDownloadUrl(batchId: string) {
|
||||
return `/admin/api/error-reports/${encodeURIComponent(batchId)}/download`;
|
||||
}
|
||||
|
||||
export async function downloadAdminErrorReport(token: string, batchId: string) {
|
||||
const response = await fetch(
|
||||
buildRequestUrl(getAdminErrorReportDownloadUrl(batchId)),
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new AdminApiError({
|
||||
message: `下载失败(${response.status})`,
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
export function getAdminFeatureGateConfig(token: string) {
|
||||
return request<AdminFeatureGateConfigResponse>('/admin/api/feature-gates', {
|
||||
token,
|
||||
@@ -800,43 +866,44 @@ async function postAdminDirectUploadFile(
|
||||
}
|
||||
}
|
||||
|
||||
function buildQueryString(query: AdminTrackingEventListQuery) {
|
||||
function buildQueryString(append: (params: URLSearchParams) => void) {
|
||||
const params = new URLSearchParams();
|
||||
appendQueryParam(params, 'eventKey', query.eventKey);
|
||||
appendQueryParam(params, 'userId', query.userId);
|
||||
appendQueryParam(params, 'scopeKind', query.scopeKind);
|
||||
appendQueryParam(params, 'scopeId', query.scopeId);
|
||||
appendQueryParam(params, 'startDate', query.startDate);
|
||||
appendQueryParam(params, 'endDate', query.endDate);
|
||||
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
||||
params.set('limit', String(query.limit));
|
||||
}
|
||||
if (query.exportAll) {
|
||||
params.set('exportAll', 'true');
|
||||
}
|
||||
const queryString = params.toString();
|
||||
return queryString ? `?${queryString}` : '';
|
||||
append(params);
|
||||
return formatQueryString(params);
|
||||
}
|
||||
|
||||
function buildErrorReportQueryString(query: {
|
||||
status?: string;
|
||||
fingerprint?: string;
|
||||
source?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
return buildQueryString((params) => {
|
||||
appendQueryParam(params, 'status', query.status);
|
||||
appendQueryParam(params, 'fingerprint', query.fingerprint);
|
||||
appendQueryParam(params, 'source', query.source);
|
||||
appendNumericQueryParam(params, 'limit', query.limit);
|
||||
appendNumericQueryParam(params, 'offset', query.offset);
|
||||
});
|
||||
}
|
||||
|
||||
function buildAdminRechargeOrderListQuery(query: AdminRechargeOrderListQuery) {
|
||||
const params = new URLSearchParams();
|
||||
appendQueryParam(params, 'orderId', query.orderId);
|
||||
appendQueryParam(
|
||||
params,
|
||||
'providerTransactionId',
|
||||
query.providerTransactionId,
|
||||
);
|
||||
appendQueryParam(params, 'userId', query.userId);
|
||||
appendQueryParam(params, 'publicUserCode', query.publicUserCode);
|
||||
appendQueryParam(params, 'paymentChannel', query.paymentChannel);
|
||||
appendQueryParam(params, 'status', query.status);
|
||||
appendQueryParam(params, 'createdAfter', query.createdAfter);
|
||||
appendQueryParam(params, 'createdBefore', query.createdBefore);
|
||||
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
||||
params.set('limit', String(query.limit));
|
||||
}
|
||||
const queryString = params.toString();
|
||||
return queryString ? `?${queryString}` : '';
|
||||
return buildQueryString((params) => {
|
||||
appendQueryParam(params, 'orderId', query.orderId);
|
||||
appendQueryParam(
|
||||
params,
|
||||
'providerTransactionId',
|
||||
query.providerTransactionId,
|
||||
);
|
||||
appendQueryParam(params, 'userId', query.userId);
|
||||
appendQueryParam(params, 'publicUserCode', query.publicUserCode);
|
||||
appendQueryParam(params, 'paymentChannel', query.paymentChannel);
|
||||
appendQueryParam(params, 'status', query.status);
|
||||
appendQueryParam(params, 'createdAfter', query.createdAfter);
|
||||
appendQueryParam(params, 'createdBefore', query.createdBefore);
|
||||
appendNumericQueryParam(params, 'limit', query.limit);
|
||||
});
|
||||
}
|
||||
|
||||
function buildAdminUserDetailQuery(query: AdminUserDetailQuery) {
|
||||
@@ -935,6 +1002,21 @@ function appendQueryParam(
|
||||
}
|
||||
}
|
||||
|
||||
function appendNumericQueryParam(
|
||||
params: URLSearchParams,
|
||||
key: string,
|
||||
value: number | null | undefined,
|
||||
) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
function formatQueryString(params: URLSearchParams) {
|
||||
const queryString = params.toString();
|
||||
return queryString ? `?${queryString}` : '';
|
||||
}
|
||||
|
||||
function parseJsonResponse(responseText: string): unknown {
|
||||
if (!responseText.trim()) {
|
||||
return null;
|
||||
|
||||
@@ -96,6 +96,38 @@ export interface AdminMeResponse {
|
||||
admin: AdminSessionPayload;
|
||||
}
|
||||
|
||||
export interface AdminErrorReportEntry {
|
||||
batchId: string;
|
||||
eventCount: number;
|
||||
logCount: number;
|
||||
fingerprint?: string;
|
||||
source?: string;
|
||||
status: 'new' | 'in-progress' | 'resolved';
|
||||
userId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
userDescription?: string;
|
||||
attachmentSizeBytes: number;
|
||||
submissionId?: string;
|
||||
ossObjectKey?: string;
|
||||
archiveSha256?: string;
|
||||
uploadStatus?: 'uploading' | 'ready' | 'failed';
|
||||
}
|
||||
|
||||
export interface AdminErrorReportListResponse {
|
||||
reports: AdminErrorReportEntry[];
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export interface AdminErrorReportDetail extends AdminErrorReportEntry {
|
||||
note?: string;
|
||||
events: Array<Record<string, unknown>>;
|
||||
logNames: string[];
|
||||
}
|
||||
|
||||
export interface AdminOverviewResponse {
|
||||
service: AdminServiceOverviewPayload;
|
||||
database: AdminDatabaseOverviewPayload;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { AdminDebugHttpPage } from '../pages/AdminDebugHttpPage';
|
||||
import { AdminEditorAssetQueryPage } from '../pages/AdminEditorAssetQueryPage';
|
||||
import { AdminEditorGenerationPricingPage } from '../pages/AdminEditorGenerationPricingPage';
|
||||
import { AdminEditorShowcaseReviewPage } from '../pages/AdminEditorShowcaseReviewPage';
|
||||
import { AdminErrorReportsPage } from '../pages/AdminErrorReportsPage';
|
||||
import { AdminGrayReleaseConfigPage } from '../pages/AdminGrayReleaseConfigPage';
|
||||
import { AdminInviteCodePage } from '../pages/AdminInviteCodePage';
|
||||
import { AdminLoginPage } from '../pages/AdminLoginPage';
|
||||
@@ -228,6 +229,12 @@ export function AdminApp() {
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'error-reports' ? (
|
||||
<AdminErrorReportsPage
|
||||
token={token}
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'gray-release' ? (
|
||||
<AdminGrayReleaseConfigPage
|
||||
token={token}
|
||||
|
||||
@@ -38,6 +38,7 @@ const routeIcons = {
|
||||
tables: Database,
|
||||
debug: Bug,
|
||||
tracking: Table2,
|
||||
'error-reports': Bug,
|
||||
'gray-release': GitBranch,
|
||||
redeem: TicketPercent,
|
||||
invite: TicketCheck,
|
||||
|
||||
@@ -5,6 +5,7 @@ export type AdminRouteId =
|
||||
| 'tables'
|
||||
| 'debug'
|
||||
| 'tracking'
|
||||
| 'error-reports'
|
||||
| 'gray-release'
|
||||
| 'redeem'
|
||||
| 'invite'
|
||||
@@ -33,6 +34,7 @@ export const adminRoutes: AdminRouteDefinition[] = [
|
||||
{ id: 'tables', label: '表查询', hash: '#tables' },
|
||||
{ id: 'debug', label: 'API 调试', hash: '#debug' },
|
||||
{ id: 'tracking', label: '埋点数据', hash: '#tracking' },
|
||||
{ id: 'error-reports', label: '错误报告', hash: '#error-reports' },
|
||||
{ id: 'gray-release', label: '灰度发布', hash: '#gray-release' },
|
||||
{ id: 'redeem', label: '兑换码', hash: '#redeem' },
|
||||
{ id: 'invite', label: '邀请码', hash: '#invite' },
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
downloadAdminErrorReport,
|
||||
formatAdminApiError,
|
||||
getAdminErrorReport,
|
||||
isAdminApiError,
|
||||
listAdminErrorReports,
|
||||
updateAdminErrorReport,
|
||||
} from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminErrorReportDetail,
|
||||
AdminErrorReportEntry,
|
||||
} from '../api/adminApiTypes';
|
||||
|
||||
type Props = { token: string; onUnauthorized: (message?: string) => void };
|
||||
|
||||
const ADMIN_ERROR_REPORT_STATUSES = ['new', 'in-progress', 'resolved'] as const;
|
||||
const ERROR_REPORT_PAGE_SIZE = 50;
|
||||
|
||||
export function AdminErrorReportsPage({ token, onUnauthorized }: Props) {
|
||||
const [reports, setReports] = useState<AdminErrorReportEntry[]>([]);
|
||||
const [selected, setSelected] = useState<AdminErrorReportDetail | null>(null);
|
||||
const [status, setStatus] = useState('');
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [filterStatus, setFilterStatus] = useState('');
|
||||
const [pageOffset, setPageOffset] = useState(0);
|
||||
const [pageInfo, setPageInfo] = useState({ total: 0, hasMore: false });
|
||||
const openReportRequestId = useRef(0);
|
||||
const loadRequestId = useRef(0);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const requestId = ++loadRequestId.current;
|
||||
setStatus('');
|
||||
try {
|
||||
const response = await listAdminErrorReports(token, {
|
||||
status: filterStatus || undefined,
|
||||
limit: ERROR_REPORT_PAGE_SIZE,
|
||||
offset: pageOffset,
|
||||
});
|
||||
if (requestId === loadRequestId.current) {
|
||||
const lastValidOffset = response.total
|
||||
? Math.floor((response.total - 1) / ERROR_REPORT_PAGE_SIZE) *
|
||||
ERROR_REPORT_PAGE_SIZE
|
||||
: 0;
|
||||
if (pageOffset > lastValidOffset) {
|
||||
setPageOffset(lastValidOffset);
|
||||
return;
|
||||
}
|
||||
setReports(response.reports);
|
||||
setPageInfo({ total: response.total, hasMore: response.hasMore });
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestId !== loadRequestId.current) return;
|
||||
if (isAdminApiError(error) && error.status === 401)
|
||||
return onUnauthorized();
|
||||
setStatus(formatAdminApiError(error));
|
||||
}
|
||||
}, [filterStatus, onUnauthorized, pageOffset, token]);
|
||||
|
||||
const loadRef = useRef(load);
|
||||
useEffect(() => {
|
||||
loadRef.current = load;
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
async function openReport(batchId: string) {
|
||||
const requestId = ++openReportRequestId.current;
|
||||
setStatus('');
|
||||
try {
|
||||
const detail = await getAdminErrorReport(token, batchId);
|
||||
if (requestId === openReportRequestId.current) setSelected(detail);
|
||||
} catch (error) {
|
||||
if (requestId !== openReportRequestId.current) return;
|
||||
if (isAdminApiError(error) && error.status === 401)
|
||||
return onUnauthorized();
|
||||
setStatus(formatAdminApiError(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function saveStatus(nextStatus: string) {
|
||||
if (!selected || busy) return;
|
||||
setBusy(true);
|
||||
setStatus('');
|
||||
try {
|
||||
const updated = await updateAdminErrorReport(token, selected.batchId, {
|
||||
status: nextStatus,
|
||||
note: selected.note,
|
||||
});
|
||||
setSelected((current) =>
|
||||
current ? { ...current, ...updated } : current,
|
||||
);
|
||||
await loadRef.current();
|
||||
} catch (error) {
|
||||
if (isAdminApiError(error) && error.status === 401) onUnauthorized();
|
||||
else setStatus(formatAdminApiError(error));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function download(batchId: string) {
|
||||
if (downloading) return;
|
||||
setDownloading(true);
|
||||
setStatus('');
|
||||
try {
|
||||
const blob = await downloadAdminErrorReport(token, batchId);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = `${batchId}.zip`;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
||||
} catch (error) {
|
||||
if (isAdminApiError(error) && error.status === 401) onUnauthorized();
|
||||
else setStatus(formatAdminApiError(error));
|
||||
} finally {
|
||||
setDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="admin-panel">
|
||||
<div className="admin-panel-header">
|
||||
<div>
|
||||
<h1>错误报告</h1>
|
||||
<p>查看用户提交的脱敏诊断包。</p>
|
||||
</div>
|
||||
<label>
|
||||
状态{' '}
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={(event) => {
|
||||
setFilterStatus(event.target.value);
|
||||
setPageOffset(0);
|
||||
}}
|
||||
>
|
||||
<option value="">全部</option>
|
||||
{ADMIN_ERROR_REPORT_STATUSES.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{status ? (
|
||||
<p className="admin-alert" role="status">
|
||||
{status}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="admin-table-wrap">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>报告</th>
|
||||
<th>事件</th>
|
||||
<th>来源</th>
|
||||
<th>状态</th>
|
||||
<th>用户</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{reports.map((report) => (
|
||||
<tr
|
||||
key={report.batchId}
|
||||
onClick={() => void openReport(report.batchId)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
void openReport(report.batchId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td>{report.batchId}</td>
|
||||
<td>{report.eventCount}</td>
|
||||
<td>{report.source ?? '-'}</td>
|
||||
<td>{report.status}</td>
|
||||
<td>{report.userId}</td>
|
||||
<td>{new Date(report.createdAt).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="admin-detail-modal__actions" aria-label="错误报告分页">
|
||||
<span>
|
||||
{pageInfo.total === 0
|
||||
? '暂无报告'
|
||||
: `第 ${pageOffset + 1}-${Math.min(pageOffset + reports.length, pageInfo.total)} 条,共 ${pageInfo.total} 条`}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setPageOffset((offset) =>
|
||||
Math.max(0, offset - ERROR_REPORT_PAGE_SIZE),
|
||||
)
|
||||
}
|
||||
disabled={pageOffset === 0}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setPageOffset((offset) => offset + ERROR_REPORT_PAGE_SIZE)
|
||||
}
|
||||
disabled={!pageInfo.hasMore}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
{selected ? (
|
||||
<div
|
||||
className="admin-detail-modal"
|
||||
role="dialog"
|
||||
aria-label="错误报告详情"
|
||||
>
|
||||
<div className="admin-detail-modal__panel">
|
||||
<header>
|
||||
<h2>{selected.batchId}</h2>
|
||||
<button type="button" onClick={() => setSelected(null)}>
|
||||
关闭
|
||||
</button>
|
||||
</header>
|
||||
<p>
|
||||
用户:{selected.userId} · 事件:{selected.eventCount} · 日志:
|
||||
{selected.logCount}
|
||||
</p>
|
||||
<pre>{JSON.stringify(selected.events.slice(0, 20), null, 2)}</pre>
|
||||
{selected.userDescription ? (
|
||||
<p>用户描述:{selected.userDescription}</p>
|
||||
) : null}
|
||||
<label className="admin-detail-modal__note">
|
||||
处理备注
|
||||
<textarea
|
||||
value={selected.note ?? ''}
|
||||
onChange={(event) =>
|
||||
setSelected((current) =>
|
||||
current
|
||||
? { ...current, note: event.target.value }
|
||||
: current,
|
||||
)
|
||||
}
|
||||
maxLength={2000}
|
||||
rows={4}
|
||||
placeholder="记录处理结论或后续跟进事项"
|
||||
disabled={busy}
|
||||
/>
|
||||
</label>
|
||||
<div className="admin-detail-modal__actions">
|
||||
<select
|
||||
value={selected.status}
|
||||
onChange={(event) => void saveStatus(event.target.value)}
|
||||
disabled={busy}
|
||||
>
|
||||
{ADMIN_ERROR_REPORT_STATUSES.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void saveStatus(selected.status)}
|
||||
disabled={busy}
|
||||
>
|
||||
保存备注
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void download(selected.batchId)}
|
||||
disabled={downloading}
|
||||
>
|
||||
{downloading ? '下载中…' : '下载诊断包'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -3075,3 +3075,23 @@ button:disabled {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.admin-detail-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
background: rgb(15 23 42 / 35%);
|
||||
}
|
||||
.admin-detail-modal__panel {
|
||||
width: min(860px, 100%);
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
background: var(--admin-surface, #fff);
|
||||
}
|
||||
.admin-detail-modal__panel header,
|
||||
.admin-detail-modal__actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.admin-detail-modal__panel pre { max-height: 360px; overflow: auto; white-space: pre-wrap; background: #f8fafc; padding: 12px; border-radius: 8px; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import {
|
||||
@@ -46,3 +47,14 @@ test('next release version follows the higher local or OSS version', () => {
|
||||
assert.equal(nextPatchVersion('0.1.18', '0.1.15'), '0.1.19');
|
||||
assert.equal(nextPatchVersion('0.1.12', null), '0.1.13');
|
||||
});
|
||||
|
||||
test('release upload forces overwrite for versioned artifact and latest pointer', () => {
|
||||
const source = readFileSync(
|
||||
new URL('./release-upload.mjs', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
assert.equal(
|
||||
(source.match(/runOssutil\(\['cp', '--force'/gu) ?? []).length,
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -320,6 +320,7 @@ const APP_INVOKE_BARE_CALL_NAMES = new Set([
|
||||
'directInvoke',
|
||||
'invokeInput',
|
||||
'invokeAuthenticatedInput',
|
||||
'invokeDiagnostic',
|
||||
]);
|
||||
|
||||
function parseAppInvokeCommandNames(source, fileName = 'fixture.tsx') {
|
||||
@@ -352,7 +353,10 @@ function parseAppInvokeCommandNames(source, fileName = 'fixture.tsx') {
|
||||
const isObjectInvoke =
|
||||
ts.isPropertyAccessExpression(expression) &&
|
||||
expression.name.text === 'invoke';
|
||||
const commandArgument = node.arguments[0];
|
||||
const commandArgument =
|
||||
ts.isIdentifier(expression) && expression.text === 'invokeDiagnostic'
|
||||
? node.arguments[1]
|
||||
: node.arguments[0];
|
||||
if (
|
||||
(isBareCall || isObjectInvoke) &&
|
||||
commandArgument !== undefined &&
|
||||
|
||||
@@ -40,7 +40,9 @@ await prepareReleaseVersion();
|
||||
runTauriBuild([]);
|
||||
const { artifact, manifestPath, manifest } = generateUpdateManifest();
|
||||
const artifactKey = `agc/${manifest.version}/${path.basename(artifact)}`;
|
||||
runOssutil(['cp', artifact, `oss://${bucket}/${artifactKey}`]);
|
||||
runOssutil(['cp', manifestPath, `oss://${bucket}/agc/latest.json`]);
|
||||
// Jenkins/ossutil 默认会在目标对象已存在时交互询问并按默认值跳过;
|
||||
// 发布清单是固定的 latest 指针,必须显式覆盖,否则流水线会误报成功但远端仍保留旧版本。
|
||||
runOssutil(['cp', '--force', artifact, `oss://${bucket}/${artifactKey}`]);
|
||||
runOssutil(['cp', '--force', manifestPath, `oss://${bucket}/agc/latest.json`]);
|
||||
console.log(`[ai-game-creator-shell] 已上传 oss://${bucket}/${artifactKey}`);
|
||||
console.log(`[ai-game-creator-shell] 已上传 oss://${bucket}/agc/latest.json`);
|
||||
|
||||
@@ -79,6 +79,7 @@ function resolveBackendTargetsFromState(
|
||||
) {
|
||||
const apiServer = state?.services?.['api-server'];
|
||||
const spacetime = state?.services?.spacetime;
|
||||
const bgfilterWorker = state?.services?.['bgfilter-worker'];
|
||||
const isActive = (service) =>
|
||||
service && ['running', 'reused', 'starting'].includes(service.status ?? '');
|
||||
const database = typeof state?.database === 'string' ? state.database : '';
|
||||
@@ -104,9 +105,14 @@ function resolveBackendTargetsFromState(
|
||||
: requireAgcBackend
|
||||
? ''
|
||||
: 'http://127.0.0.1:3101';
|
||||
const bgfilterWorkerUrl =
|
||||
canReuseState && isActive(bgfilterWorker) && bgfilterWorker.url
|
||||
? bgfilterWorker.url
|
||||
: '';
|
||||
return {
|
||||
apiUrl,
|
||||
spacetimeUrl,
|
||||
bgfilterWorkerUrl,
|
||||
database,
|
||||
spacetimeDataDir,
|
||||
hasMatchingDatabase,
|
||||
@@ -121,16 +127,22 @@ function readBackendTargets({ requireAgcBackend = false } = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
async function isBackendReady() {
|
||||
const { apiUrl, spacetimeUrl, hasMatchingBackend } = readBackendTargets({
|
||||
requireAgcBackend: true,
|
||||
});
|
||||
async function isBackendReady({
|
||||
state = readJson(devStackStatePath),
|
||||
isReady = isHttpReady,
|
||||
} = {}) {
|
||||
const { apiUrl, spacetimeUrl, bgfilterWorkerUrl, hasMatchingBackend } =
|
||||
resolveBackendTargetsFromState(state, {
|
||||
requireAgcBackend: true,
|
||||
});
|
||||
return (
|
||||
hasMatchingBackend &&
|
||||
Boolean(apiUrl) &&
|
||||
Boolean(spacetimeUrl) &&
|
||||
(await isHttpReady(`${apiUrl}/healthz`)) &&
|
||||
(await isHttpReady(`${spacetimeUrl}/v1/ping`))
|
||||
Boolean(bgfilterWorkerUrl) &&
|
||||
(await isReady(`${apiUrl}/healthz`)) &&
|
||||
(await isReady(`${spacetimeUrl}/v1/ping`)) &&
|
||||
(await isReady(`${bgfilterWorkerUrl}/readyz`))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -668,6 +680,7 @@ function isDirectModuleExecution() {
|
||||
export {
|
||||
ensureBackend,
|
||||
formatChildFailure,
|
||||
isBackendReady,
|
||||
isDirectModuleExecution,
|
||||
isProcessGroupAlive,
|
||||
preflightExistingVite,
|
||||
|
||||
+1
@@ -1727,6 +1727,7 @@ dependencies = [
|
||||
"platform-agent",
|
||||
"platform-llm",
|
||||
"portable-pty",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
"schemars 1.2.1",
|
||||
"serde",
|
||||
|
||||
@@ -44,6 +44,7 @@ platform-agent = { path = "../../../server-rs/crates/platform-agent" }
|
||||
portable-pty = "0.9"
|
||||
percent-encoding = "2"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "native-tls", "stream"] }
|
||||
regex = "1"
|
||||
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
|
||||
tauri = { version = "2.11.2", features = [] }
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ Use `agc_browser_playtest` from the `agc_tools` MCP server. Do not replace it wi
|
||||
5. Treat browser infrastructure failure, an unloaded page, an unhandled exception, or missing evidence as a failed validation. Do not claim success from a partial result.
|
||||
6. Use game-specific reasoning for quality. Do not require a fixed board, fixed text, fixed number of slices, or a legacy harness scenario; the tool result is evidence for Codex to interpret.
|
||||
|
||||
Call `agc_read_skill_resource` with `skillName="agc-browser-playtest"` and `relativePath="references/browser-evidence-contract.md"` before interpreting screenshots or Canvas/WebGL asset observations.
|
||||
Before interpreting screenshots or Canvas/WebGL asset observations, read `references/browser-evidence-contract.md`.
|
||||
|
||||
## Reporting
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ Let the client derive projections from real disk changes and trusted tool result
|
||||
10. Do not claim a resource or version is visible before the client projects it. If projection is missing, report the changed relative files and let the client re-read durable state.
|
||||
11. Never move HTML, CSS, or JavaScript into documentation folders. They belong to the game-code projection; prose, design notes, and instructions remain documents.
|
||||
|
||||
Call `agc_read_skill_resource` with `skillName="agc-client-projection"` and `relativePath="references/projection-contract.md"` when a request touches asset identity, revision behavior, or version history.
|
||||
When a request touches asset identity, revision behavior, or version history, read `references/projection-contract.md`.
|
||||
|
||||
## Boundaries
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ Treat the current working directory as the only project root.
|
||||
6. Reuse existing files and asset identities. Do not create a second project root, hidden harness, Supervisor workspace, or parallel implementation.
|
||||
7. Make the smallest coherent change that satisfies the user request, then inspect the actual changed files.
|
||||
|
||||
Call `agc_read_skill_resource` with `skillName="agc-project-structure"` and `relativePath="references/structure-contract.md"` when deciding where a new file belongs or whether a state file may be edited.
|
||||
When deciding where a new file belongs or whether a state file may be edited, read `references/structure-contract.md`.
|
||||
|
||||
## Boundaries
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ Implement the user's actual game request in the current project. Choose DOM, Can
|
||||
6. Avoid undefined animation callbacks, duplicate loops, stale event listeners, and state that survives restart unintentionally.
|
||||
7. After a meaningful game change, use the browser playtest Skill and fix issues shown by real evidence before reporting completion.
|
||||
|
||||
Call `agc_read_skill_resource` with `skillName="agc-web-game-development"` and `relativePath="references/game-quality-checklist.md"` when implementing a new game loop or a broad gameplay revision.
|
||||
When implementing a new game loop or a broad gameplay revision, read `references/game-quality-checklist.md`.
|
||||
|
||||
# Notes
|
||||
* UI Editor's JSON formated is a special kind asset; Never edit it or read it directly. User can export to `ui/generated-<digest>.js` and want u to use it;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": "agc-skill-pack.v1",
|
||||
"version": "2026-08-26.9",
|
||||
"version": "2026-08-26.10",
|
||||
"skills": [
|
||||
{
|
||||
"name": "agc-project-structure",
|
||||
@@ -11,7 +11,6 @@
|
||||
"读取项目状态证据"
|
||||
],
|
||||
"requiredTools": [
|
||||
"agc_tools.agc_read_skill_resource",
|
||||
"agc_tools.agc_list_project_files",
|
||||
"agc_tools.agc_import_account_assets",
|
||||
"agc_tools.agc_list_registered_assets"
|
||||
@@ -21,7 +20,7 @@
|
||||
"agents/openai.yaml",
|
||||
"references/structure-contract.md"
|
||||
],
|
||||
"sha256": "85dd861201d7b9a5d34b702b7b79ce18012e06784dddf2fb8e836e085fee4b00"
|
||||
"sha256": "54cfe7da2e7e1f631788e87c5ccc481f29e5b0d70cd32f9fe8092a5aed4549ac"
|
||||
},
|
||||
{
|
||||
"name": "taonier-art-assets",
|
||||
@@ -32,7 +31,6 @@
|
||||
"已有陶泥儿素材需要接入玩法"
|
||||
],
|
||||
"requiredTools": [
|
||||
"agc_tools.agc_read_skill_resource",
|
||||
"agc_tools.agc_generate_image",
|
||||
"agc_tools.agc_edit_image",
|
||||
"agc_tools.taonier_prepare_game_art"
|
||||
@@ -42,7 +40,7 @@
|
||||
"agents/openai.yaml",
|
||||
"references/platform-art-contract.md"
|
||||
],
|
||||
"sha256": "600c758662de7c85186402fd09ba3e76407ad2f6084d4090a2a38f455da7ea09"
|
||||
"sha256": "82e4b2ee8ca8147b51ca206b0565b3cc244dc5d3cddb8343875001c0beb4711f"
|
||||
},
|
||||
{
|
||||
"name": "agc-web-game-development",
|
||||
@@ -53,15 +51,13 @@
|
||||
"调整布局与交互",
|
||||
"接入已有素材"
|
||||
],
|
||||
"requiredTools": [
|
||||
"agc_tools.agc_read_skill_resource"
|
||||
],
|
||||
"requiredTools": [],
|
||||
"files": [
|
||||
"SKILL.md",
|
||||
"agents/openai.yaml",
|
||||
"references/game-quality-checklist.md"
|
||||
],
|
||||
"sha256": "b197a242b75dc5e39739693e793acd25ac62dd2c72bc264305797936da47f356"
|
||||
"sha256": "d7748d9ebf4324add0541daf16a2bbec09c4862b85af55bfb369c7f3b99aedff"
|
||||
},
|
||||
{
|
||||
"name": "agc-browser-playtest",
|
||||
@@ -72,7 +68,6 @@
|
||||
"声明游戏可玩前验收"
|
||||
],
|
||||
"requiredTools": [
|
||||
"agc_tools.agc_read_skill_resource",
|
||||
"agc_tools.agc_browser_playtest"
|
||||
],
|
||||
"files": [
|
||||
@@ -80,7 +75,7 @@
|
||||
"agents/openai.yaml",
|
||||
"references/browser-evidence-contract.md"
|
||||
],
|
||||
"sha256": "a68fc43f460ec1b8f999bda089c8e67a7229ef70365cfe89b92030e7661c47d5"
|
||||
"sha256": "4437cd8a927a1c79a5faf4bcd40e9946676c08a3b460ab171298cabf899f49ad"
|
||||
},
|
||||
{
|
||||
"name": "agc-client-projection",
|
||||
@@ -93,7 +88,6 @@
|
||||
"推理 revision 与版本关系"
|
||||
],
|
||||
"requiredTools": [
|
||||
"agc_tools.agc_read_skill_resource",
|
||||
"agc_tools.agc_list_registered_assets",
|
||||
"agc_tools.agc_list_project_files",
|
||||
"agc_tools.agc_list_account_assets",
|
||||
@@ -106,7 +100,7 @@
|
||||
"agents/openai.yaml",
|
||||
"references/projection-contract.md"
|
||||
],
|
||||
"sha256": "7800e8ee4b5baa6f473b904f262197f5320a46150ae5e51c56a298fc21674e30"
|
||||
"sha256": "96b5bf9e2ed150bbe934a888867c1bb500b214a131f8b36c4830f51ca30267b6"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ the complete game-art package and its canonical slices.
|
||||
5. Inspect the returned background, complete sheet, and available slice previews before integrating them. Then use suitable returned runtime assets in the game's actual visible experience and confirm their visible use in desktop and mobile playtest evidence. `art-spec.png` is a reference specification, not a runtime background, character, prop, or effect. Background exclusions, seamless tiling, entity semantics, and final draw dimensions are visual/runtime acceptance checks; a prompt alone does not prove them. A hidden or side-panel preview does not count as gameplay use.
|
||||
6. Preserve warning details in the final report. If the tool reports missing credentials, uncertain operation state, invalid provenance, download failure, or decode failure, stop and report the actionable reason; do not substitute generated CSS shapes and call the platform step complete.
|
||||
|
||||
Call `agc_read_skill_resource` with `skillName="taonier-art-assets"` and `relativePath="references/platform-art-contract.md"` before interpreting async recovery, source-preserved warnings, or slice warnings.
|
||||
Before interpreting async recovery, source-preserved warnings, or slice warnings, read `references/platform-art-contract.md`.
|
||||
|
||||
## Boundaries
|
||||
|
||||
|
||||
@@ -2032,7 +2032,7 @@ impl CodexAppServerConnection {
|
||||
None
|
||||
};
|
||||
if std::env::var_os("GENARRATIVE_AGC_DIRECT_DEBUG").is_some() {
|
||||
eprintln!(
|
||||
app_log!(
|
||||
"agent.direct_codex.provider_proxy configured={}",
|
||||
provider_proxy.is_some()
|
||||
);
|
||||
@@ -2176,7 +2176,7 @@ impl CodexAppServerConnection {
|
||||
.await
|
||||
.map_err(platform_llm::LlmError::Transport)?;
|
||||
if let Some(reason) = remote_control_disable_reason {
|
||||
eprintln!("agent.codex_app_server.remote_control disabled reason={reason}");
|
||||
app_log!("agent.codex_app_server.remote_control disabled reason={reason}");
|
||||
}
|
||||
if let Some(skill_roots) = connection.inner._skill_roots.as_ref() {
|
||||
connection
|
||||
@@ -3354,7 +3354,7 @@ async fn fail_game_creator_codex_app_server_connection(
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let stderr = inner.stderr_summary.lock().await.diagnostic();
|
||||
let diagnostic = format!("{error};exitStatus={exit_status};{stderr}");
|
||||
eprintln!("agent.runner.failed: Codex app-server 连接终止:{diagnostic}");
|
||||
app_log!("agent.runner.failed: Codex app-server 连接终止:{diagnostic}");
|
||||
for (_, pending) in inner.pending.lock().await.drain() {
|
||||
let _ = pending.sender.send(Err(diagnostic.clone()));
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ async fn proxy_codex_provider_request(
|
||||
}
|
||||
let direct_debug = std::env::var_os("GENARRATIVE_AGC_DIRECT_DEBUG").is_some();
|
||||
if direct_debug {
|
||||
eprintln!(
|
||||
app_log!(
|
||||
"agent.direct_codex.provider_proxy.request method={} path={}",
|
||||
request.method(),
|
||||
request.uri().path(),
|
||||
@@ -174,7 +174,7 @@ async fn proxy_codex_provider_request(
|
||||
}
|
||||
}
|
||||
if direct_debug {
|
||||
eprintln!(
|
||||
app_log!(
|
||||
"agent.direct_codex.provider_proxy.response status={} strippedCodexHeaders={}",
|
||||
status.as_u16(),
|
||||
stripped_limit_headers,
|
||||
|
||||
@@ -639,10 +639,6 @@ fn extract_mcp_arguments(root: &Path, tool: &str, arguments: &Value) -> Value {
|
||||
copy_truncated_string(object, "query", DIRECT_CODEX_AUDIT_QUERY_CHARS, &mut out);
|
||||
copy_number(object, "maxResults", &mut out);
|
||||
}
|
||||
"agc_read_skill_resource" => {
|
||||
copy_string(object, "skillName", &mut out);
|
||||
copy_string(object, "relativePath", &mut out);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Value::Object(out)
|
||||
|
||||
@@ -10,7 +10,7 @@ const MAX_DIRECT_SYSTEM_PROMPT_CHARS: usize = 16 * 1024;
|
||||
const MIN_DIRECT_CLIENT_TURN_ID_CHARS: usize = 6;
|
||||
const MAX_DIRECT_CLIENT_TURN_ID_CHARS: usize = 160;
|
||||
const DIRECT_TAONIER_IDENTITY_GUIDANCE: &str = "对外身份合同:你是“陶泥儿”,是 Genarrative 的游戏创作助手。用户询问你是谁、你的名称或能力时,以陶泥儿的身份回答;不要把 Codex、ChatGPT、OpenAI、模型、通用 AI 助手或内部执行智能体当作自己的名称或对外身份。Codex app-server 仅是客户端内部执行技术;只有用户明确询问底层实现时才可如实说明,同时仍以陶泥儿自称。";
|
||||
const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同(仅说明项目边界,不是流程门槛):当前 Codex cwd 是用户选择的项目目录(工作区根),源码、素材、音效和其它资源按项目现有结构放置;先按需读取当前 cwd 下适用的 `AGENTS.md`、README 或项目说明,把它们当作项目规范参考。原生文件工具、patch 和命令参数使用 cwd 相对路径,例如 `index.html`、`style.css`、`game.js`、`assets/hero.png`;如果 Codex 原生文件修改不可用,可以按需用客户端 `agc_write_file` 把文本写入项目相对路径。调用 `agc_write_file` 时,content 必须是目标文件的完整原始 UTF-8 正文;不得把 command.exec 的 Exit code、Wall time、Output 包装、终端日志或解释文字一起复制进 content,命令结果只能用于判断,不能当作文件正文。`../`、绝对路径、`.agent/`、`.git/`、密钥文件和 Runtime 控制面属于客户端边界,不能请求扩权或直接改写。DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill、经客户端注入的 `agc_tools` MCP,以及客户端扩展列表中用户已启用的第三方 MCP。用户明确指定第三方 MCP Server 或工具时,先在当前可用工具中查找并直接调用;找不到时如实说明,不得伪造。你可以按需选择这些能力:`agc_write_file` 写入代码、配置、资源依赖清单或说明文件;`agc_generate_image` 生成普通图片、角色图、视觉规范图(icon-spec)、UI 设计图或发布宣传图;`agc_edit_image` 修改已登记图片;`taonier_prepare_game_art` 准备完整游戏美术包及可用的 canonical 切片;`agc_list_registered_assets`、`agc_list_project_files`、`agc_list_account_assets`、`agc_import_account_assets` 用于发现和接入资源依赖;`agc_create_or_derive_resource` 用于视频、角色动画、音效或背景音乐;`agc_browser_playtest` 用于需要时的本地试玩观察;`agc_read_skill_resource` 用于按需读取审核 Skill。切图、资源依赖、规范图和试玩都只是可选工具提示,不要求调用、固定顺序或特定产物,AGC 不会据此替你拆任务、编排 DAG、做强验收或阻止继续执行;不要等待 Supervisor、harness 或宿主规划器。不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径;项目锁、付费提交、幂等键、下载校验和客户端投影由客户端处理。游戏文件真实变化后客户端可登记资源和版本,Codex 不直接保存或伪造项目版本。";
|
||||
const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同(仅说明项目边界,不是流程门槛):当前 Codex cwd 是用户选择的项目目录(工作区根),源码、素材、音效和其它资源按项目现有结构放置;先按需读取当前 cwd 下适用的 `AGENTS.md`、README 或项目说明,把它们当作项目规范参考。原生文件工具、patch 和命令参数使用 cwd 相对路径,例如 `index.html`、`style.css`、`game.js`、`assets/hero.png`;如果 Codex 原生文件修改不可用,可以按需用客户端 `agc_write_file` 把文本写入项目相对路径。调用 `agc_write_file` 时,content 必须是目标文件的完整原始 UTF-8 正文;不得把 command.exec 的 Exit code、Wall time、Output 包装、终端日志或解释文字一起复制进 content,命令结果只能用于判断,不能当作文件正文。`../`、绝对路径、`.agent/`、`.git/`、密钥文件和 Runtime 控制面属于客户端边界,不能请求扩权或直接改写。DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill、经客户端注入的 `agc_tools` MCP,以及客户端扩展列表中用户已启用的第三方 MCP。用户明确指定第三方 MCP Server 或工具时,先在当前可用工具中查找并直接调用;找不到时如实说明,不得伪造。你可以按需选择这些能力:`agc_write_file` 写入代码、配置、资源依赖清单或说明文件;`agc_generate_image` 生成普通图片、角色图、视觉规范图(icon-spec)、UI 设计图或发布宣传图;`agc_edit_image` 修改已登记图片;`taonier_prepare_game_art` 准备完整游戏美术包及可用的 canonical 切片;`agc_list_registered_assets`、`agc_list_project_files`、`agc_list_account_assets`、`agc_import_account_assets` 用于发现和接入资源依赖;`agc_create_or_derive_resource` 用于视频、角色动画、音效或背景音乐;`agc_browser_playtest` 用于需要时的本地试玩观察;Skill references 按需使用相对路径直接读取。切图、资源依赖、规范图和试玩都只是可选工具提示,不要求调用、固定顺序或特定产物,AGC 不会据此替你拆任务、编排 DAG、做强验收或阻止继续执行;不要等待 Supervisor、harness 或宿主规划器。不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径;项目锁、付费提交、幂等键、下载校验和客户端投影由客户端处理。游戏文件真实变化后客户端可登记资源和版本,Codex 不直接保存或伪造项目版本。";
|
||||
const DIRECT_CODEX_ART_SPEC_ASSET_PATH: &str = "assets/art-spec.png";
|
||||
const DIRECT_CODEX_BACKGROUND_ASSET_PATH: &str = "assets/direct-game-background.png";
|
||||
const DIRECT_CODEX_SPRITESHEET_ASSET_PATH: &str = "assets/art-spritesheet.png";
|
||||
|
||||
@@ -43,26 +43,6 @@ fn direct_tools_mcp_specs() -> Value {
|
||||
|
||||
fn direct_tools_mcp_specs_for(controlled_web_search: bool) -> Value {
|
||||
let tools = vec![
|
||||
json!({
|
||||
"name": "agc_read_skill_resource",
|
||||
"description": "按需读取审核 AGC Skill 直接引用的一层 Markdown 文件。只能访问内置清单声明的 Skill 与 references 路径,不能读取项目、宿主或凭据文件。",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"skillName": {
|
||||
"type": "string",
|
||||
"enum": AGC_SKILL_PACK_EXPECTED_NAMES
|
||||
},
|
||||
"relativePath": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 256
|
||||
}
|
||||
},
|
||||
"required": ["skillName", "relativePath"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "agc_write_file",
|
||||
"description": "把文本写入当前 AGC 项目的相对路径。Codex 可以按需使用它直接推进代码、配置、资源依赖或说明文件;客户端只负责项目路径和基本控制面边界,不要求固定文件、任务顺序、验证或完成回执。",
|
||||
@@ -392,18 +372,6 @@ pub(in crate::agent) fn controlled_web_search_enabled() -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn call_agc_read_skill_resource(arguments: &Value) -> Value {
|
||||
let result = (|| {
|
||||
let skill_name = bounded_tool_string(arguments, "skillName", 64)?;
|
||||
let relative_path = bounded_tool_string(arguments, "relativePath", 256)?;
|
||||
read_agc_skill_resource(&skill_name, &relative_path)
|
||||
})();
|
||||
match result {
|
||||
Ok(text) => mcp_tool_result(text, Vec::new(), false),
|
||||
Err(error) => mcp_tool_result(error, Vec::new(), true),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call_agc_write_file(arguments: &Value) -> Value {
|
||||
if let Err(error) = validate_write_file_arguments(arguments) {
|
||||
return mcp_tool_result(error, Vec::new(), true);
|
||||
@@ -1066,7 +1034,6 @@ async fn handle_direct_tools_mcp_request(_root: &Path, request: Value) -> Option
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({}));
|
||||
let result = match tool {
|
||||
"agc_read_skill_resource" => call_agc_read_skill_resource(&arguments),
|
||||
"agc_write_file" => call_agc_write_file(&arguments).await,
|
||||
"taonier_prepare_game_art" => call_taonier_prepare_game_art(&arguments).await,
|
||||
"agc_generate_image" => call_agc_generate_image(&arguments).await,
|
||||
@@ -1243,7 +1210,6 @@ mod tests {
|
||||
assert_eq!(
|
||||
names,
|
||||
vec![
|
||||
"agc_read_skill_resource",
|
||||
"agc_write_file",
|
||||
"taonier_prepare_game_art",
|
||||
"agc_generate_image",
|
||||
|
||||
@@ -462,7 +462,7 @@ pub(crate) async fn request_generator_game_draft_with_client(
|
||||
Ok(response) => break response,
|
||||
Err(platform_llm::LlmError::EmptyResponse) if empty_retries < MAX_EMPTY_RETRIES => {
|
||||
empty_retries += 1;
|
||||
eprintln!(
|
||||
app_log!(
|
||||
"llm.chat.generator.empty-response 重试 {empty_retries}/{MAX_EMPTY_RETRIES}(上游返回空 content,原样重发)"
|
||||
);
|
||||
// 同步推送到 App 进度面板,便于在界面上看到重试(无需盯命令行)。
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user