67 lines
6.2 KiB
JavaScript
67 lines
6.2 KiB
JavaScript
import { validateCreation } from './creation.mjs'
|
|
export const DESKTOP_CLIENT_ID = 'jiansun-detail-desktop'
|
|
export const DESKTOP_SCOPES = ['projects:read', 'projects:write', 'assets:read', 'assets:write', 'models:read']
|
|
export class DesktopClientError extends Error {
|
|
constructor(message, status = 400, code = 'invalid_request') { super(message); this.status = status; this.code = code }
|
|
}
|
|
export function desktopAssert(condition, message, status = 400, code = 'invalid_request') { if (!condition) throw new DesktopClientError(message, status, code) }
|
|
export function desktopId(value) { desktopAssert(typeof value === 'string' && /^[a-zA-Z0-9_-]{1,80}$/.test(value), '项目编号无效'); return value }
|
|
export function desktopRedirect(value) {
|
|
let url; try { url = new URL(value) } catch { throw new DesktopClientError('客户端回调地址无效') }
|
|
desktopAssert(url.protocol === 'http:' && url.hostname === '127.0.0.1' && Number(url.port) >= 1024 && Number(url.port) <= 65535 && url.pathname === '/oauth/callback' && !url.search && !url.hash && !url.username && !url.password, '仅允许当前电脑的客户端回调地址')
|
|
return url.href
|
|
}
|
|
export function desktopAuthorization(input) {
|
|
desktopAssert(input.client_id === DESKTOP_CLIENT_ID && input.response_type === 'code' && input.code_challenge_method === 'S256', '授权请求不符合客户端协议')
|
|
desktopAssert(typeof input.state === 'string' && /^[\w-]{32,128}$/.test(input.state), '授权状态无效')
|
|
desktopAssert(typeof input.code_challenge === 'string' && /^[\w-]{43}$/.test(input.code_challenge), '客户端校验参数无效')
|
|
return { clientId: DESKTOP_CLIENT_ID, redirectUri: desktopRedirect(input.redirect_uri), state: input.state, challenge: input.code_challenge }
|
|
}
|
|
export function desktopPortal(value) {
|
|
const url = new URL(value)
|
|
desktopAssert(!url.username && !url.password && url.pathname === '/' && !url.search && !url.hash, '素材库地址应只包含域名和端口')
|
|
desktopAssert((url.protocol === 'https:' && url.hostname === 'sc.jiansun.vip' && !url.port) || (url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && Number(url.port) >= 1024), '只允许正式素材库或本机开发服务')
|
|
return url.origin
|
|
}
|
|
export function desktopProjectBody(input) {
|
|
desktopAssert(input && typeof input === 'object' && JSON.stringify(input).length <= 1_500_000, '项目内容过大')
|
|
const doc = input.document
|
|
desktopAssert([1,2,3].includes(doc?.version) && typeof doc.name === 'string' && doc.name.length <= 120 && Array.isArray(doc.screens) && doc.screens.length > 0 && doc.screens.length <= 12, '客户端项目格式无效')
|
|
if (doc.version === 2) Object.assign(doc, validateCreation(doc))
|
|
const width = doc.screens[0].width
|
|
desktopAssert([750, 1440].includes(width) && doc.screens.every(s => s.width === width && Number.isInteger(s.height) && s.height >= 400 && s.height <= 4608 && (doc.version === 3 || Array.isArray(s.layers) && s.layers.length <= 100)) && doc.screens.reduce((sum,s) => sum + s.height,0) <= 30000, '项目画布超出限制')
|
|
desktopAssert(['draft', 'active', 'completed'].includes(input.status) && Number.isInteger(input.revision) && input.revision >= 0, '项目状态或版本无效')
|
|
const conversation = input.conversation || []
|
|
desktopAssert(Array.isArray(conversation) && conversation.length <= 200 && conversation.every(m => typeof m.id === 'string' && ['user','assistant'].includes(m.role) && typeof m.text === 'string' && m.text.length <= 16000), '对话记录无效')
|
|
desktopAssert(conversation.reduce((sum,m) => sum + m.text.length,0) <= 180000, '对话记录过大,请新开对话')
|
|
const composer=input.composer||''
|
|
desktopAssert(typeof composer==='string'&&composer.length<=6000,'对话草稿过长')
|
|
return { document: doc.version === 3 ? nativeDocument(doc, input.status) : doc, conversation, composer, status: input.status, revision: input.revision, sourceWebId: input.sourceWebId ? desktopId(input.sourceWebId) : null }
|
|
}
|
|
|
|
/** Opaque native files are never decoded, executed or rendered by the platform. */
|
|
function nativeDocument(doc, status) {
|
|
desktopId(doc.id)
|
|
const native = doc.native
|
|
desktopAssert(native?.format === 'photocraft' && native.schemaVersion === 1 && Array.isArray(native.files) && native.files.length <= 16, '原生工程清单无效')
|
|
const allowed = new Set(['canvas.pcraft', 'context.json', 'session-backup.json'])
|
|
const files = native.files.map(file => {
|
|
desktopAssert(file && allowed.has(file.path) && /^native:[a-f0-9]{64}$/.test(file.source) && file.source === 'native:' + file.sha256 && Number.isSafeInteger(file.size) && file.size > 0 && file.size <= 512 * 1024 * 1024, '原生工程文件无效')
|
|
return { path: file.path, source: file.source, sha256: file.sha256, size: file.size }
|
|
})
|
|
desktopAssert(new Set(files.map(file => file.path)).size === files.length, '原生工程文件重复')
|
|
const documentSource = native.documentSource ?? null
|
|
desktopAssert(documentSource === null || files.some(file => file.source === documentSource && file.path === 'canvas.pcraft'), '原生画布引用无效')
|
|
const screens = doc.screens.map(screen => {
|
|
desktopId(screen.id)
|
|
desktopAssert(screen.previewSource == null || /^local:[a-zA-Z0-9-]{1,100}$/.test(screen.previewSource), '原生预览图片引用无效')
|
|
desktopAssert(screen.name === undefined || typeof screen.name === 'string' && screen.name.length <= 120, '分屏名称无效')
|
|
return { id: screen.id, name: screen.name || '', width: screen.width, height: screen.height, previewSource: screen.previewSource ?? null }
|
|
})
|
|
desktopAssert(new Set(screens.map(screen => screen.id)).size === screens.length, '分屏编号重复')
|
|
desktopAssert(!documentSource || ['canvas.pcraft','context.json','session-backup.json'].every(name => files.some(file => file.path === name)), '原生工程必须同时备份画布、产品资料和会话恢复文件')
|
|
desktopAssert(status !== 'completed' || documentSource && screens.every(screen => screen.previewSource), '完结项目必须先保存原生工程和全部分屏预览')
|
|
return { version: 3, id: doc.id, name: doc.name, native: { format: 'photocraft', schemaVersion: 1, documentSource, files }, screens,
|
|
...(doc.creation ? { creation: validateCreation(doc.creation) } : {}) }
|
|
}
|