更新chatgpt协议

This commit is contained in:
2026-07-06 10:21:44 +08:00
parent c96bee4094
commit e517c56225
5 changed files with 78 additions and 21 deletions
+1 -1
View File
@@ -223,7 +223,7 @@ func readMultipartImages(c *gin.Context, keys ...string) []string {
if e != nil {
continue
}
b, _ := io.ReadAll(io.LimitReader(f, 8<<20+1))
b, _ := io.ReadAll(io.LimitReader(f, 20<<20+1))
f.Close()
if len(b) > 0 {
out = append(out, base64.StdEncoding.EncodeToString(b))
+34 -6
View File
@@ -73,6 +73,15 @@ func (c *Client) GenerateImage(ctx context.Context, accessToken, prompt, model,
return nil, nil, err
}
// Fail over immediately when the account's image_gen allowance is spent:
// submitting anyway just burns the whole poll budget and surfaces as
// "image poll timeout". Unknown quota (init failed) proceeds as before.
if quota, qErr := c.fetchImageQuota(ctx, session, accessToken); qErr == nil && quota["unknown"] == false {
if remaining, ok := quota["remaining"].(int); ok && remaining <= 0 {
return nil, nil, fmt.Errorf("%w: image_gen remaining 0 (resets %s)", ErrQuotaExhausted, stringValue(quota["reset_after"]))
}
}
scriptSources, dataBuild, err := c.bootstrap(ctx, session)
if err != nil {
return nil, nil, err
@@ -142,6 +151,10 @@ func (c *Client) FetchImageQuota(ctx context.Context, accessToken string) (map[s
if err != nil {
return nil, err
}
return c.fetchImageQuota(ctx, session, accessToken)
}
func (c *Client) fetchImageQuota(ctx context.Context, session tlsclient.HttpClient, accessToken string) (map[string]any, error) {
path := "/backend-api/conversation/init"
body, _ := json.Marshal(map[string]any{
"gizmo_id": nil,
@@ -949,12 +962,27 @@ func (c *Client) pollForImage(ctx context.Context, session tlsclient.HttpClient,
}
func (c *Client) getFileDownloadURL(ctx context.Context, session tlsclient.HttpClient, accessToken, conversationID, fileID string, inline bool) (string, error) {
// Python parity (_get_file_download_url): GET /backend-api/files/{id}/download
// with NO query params. The old /files/download/{id}?conversation_id&inline
// form returned an inline stream URL that 403s with "File stream access denied".
_ = conversationID
_ = inline
path := "/backend-api/files/" + fileID + "/download"
// Current web client form: GET /backend-api/files/download/{id}
// ?conversation_id=...&inline=false → {"status":"success","download_url":...}.
// Falls back to the legacy /files/{id}/download form if the new one fails.
paths := []string{
"/backend-api/files/download/" + fileID + "?conversation_id=" + conversationID + "&inline=" + strconv.FormatBool(inline),
"/backend-api/files/" + fileID + "/download",
}
var lastErr error
for _, path := range paths {
rawURL, err := c.fetchDownloadURL(ctx, session, accessToken, path)
if err == nil && rawURL != "" {
return rawURL, nil
}
if err != nil {
lastErr = err
}
}
return "", lastErr
}
func (c *Client) fetchDownloadURL(ctx context.Context, session tlsclient.HttpClient, accessToken, path string) (string, error) {
req, err := http.NewRequest(http.MethodGet, baseURL+path, nil)
if err != nil {
return "", err
+16
View File
@@ -43,6 +43,22 @@ var (
"this request may violate our content polic",
"this prompt may violate our content polic",
"may violate our content policies",
"i can't help with",
"i can\u2019t help with",
"i cannot help with",
"i can't assist with",
"i can\u2019t assist with",
"i cannot assist with",
"i'm unable to help with",
"i\u2019m unable to help with",
"can't create images",
"can\u2019t create images",
"cannot create images",
"can't generate images",
"can\u2019t generate images",
"cannot generate images",
"unable to create images",
"unable to generate images",
}
)
+2 -2
View File
@@ -58,10 +58,10 @@ var (
ErrVideoNotReady = errors.New("video is not ready yet")
)
// maxReferenceImageBytes bounds a single decoded reference image. 8 MB
// maxReferenceImageBytes bounds a single decoded reference image. 20 MB
// comfortably covers real photos/screenshots; anything larger is almost
// certainly abuse or a mistake. Mirrors Python core/refs.py.
const maxReferenceImageBytes = 8 * 1024 * 1024
const maxReferenceImageBytes = 20 * 1024 * 1024
type V1Service struct {
cfg *config.Config
+25 -12
View File
@@ -181,16 +181,18 @@ function setMode(m) {
}
function openPicker() { fileInput.value && fileInput.value.click() }
// Backend rejects reference images over 8MB (maxReferenceImageBytes). Enforce it
// Backend rejects reference images over 20MB (maxReferenceImageBytes). Enforce it
// here at pick time so an oversized image fails fast with a clear message instead
// of charging + failing upstream after the upload.
const MAX_REF_BYTES = 8 * 1024 * 1024
const MAX_REF_BYTES = 20 * 1024 * 1024
function onFiles(ev) {
addFiles(Array.from(ev.target.files || []))
if (ev.target) ev.target.value = ''
}
// Shared by the file picker AND drag-and-drop. Filters to images, honors the
// per-model max + 8MB cap, reads each to a data URL.
// per-model max + 20MB cap. The preview renders the picked file directly via an
// object URL (browser scales it down, no base64 copy in the DOM); the ORIGINAL
// file is kept untouched and is what gets uploaded at submit time.
function addFiles(files) {
files = files.filter((f) => f && f.type && f.type.startsWith('image/'))
const room = Math.max(0, maxRefs.value - refImages.value.length)
@@ -199,13 +201,11 @@ function addFiles(files) {
for (const f of files) {
if (added >= room) break
if (f.size > MAX_REF_BYTES) { tooBig.push(f.name); continue }
const reader = new FileReader()
reader.onload = () => refImages.value.push({ name: f.name, dataUrl: reader.result })
reader.readAsDataURL(f)
refImages.value.push({ name: f.name, file: f, thumb: URL.createObjectURL(f) })
added++
}
error.value = tooBig.length
? `图片超过 8MB 已跳过:${tooBig.join('、')}(请压缩后再传)`
? `图片超过 20MB 已跳过:${tooBig.join('、')}(请压缩后再传)`
: ''
}
// Drag-and-drop onto the reference area.
@@ -225,7 +225,10 @@ function onDragLeave(ev) {
if (ev.currentTarget.contains(ev.relatedTarget)) return
dragOver.value = false
}
function removeRef(i) { refImages.value.splice(i, 1) }
function removeRef(i) {
const [gone] = refImages.value.splice(i, 1)
if (gone?.thumb?.startsWith('blob:')) URL.revokeObjectURL(gone.thumb)
}
// Re-hydrate reference thumbnails from server URLs (after a reload). Fetches
// each /images URL (same-origin, cookie-authed) and converts to a data URL so
@@ -240,10 +243,20 @@ function restoreRefs(urls) {
refImages.value = urls.map((u) => ({ name: 'ref', url: u }))
}
// refToBase64 yields the raw base64 the backend expects, from either a freshly
// uploaded ref (dataUrl) or a restored one (url → fetch). Returns '' on failure.
// refToBase64 yields the raw base64 the backend expects, from a freshly picked
// ref (file — the untouched original), a data-URL ref (pasted/frame captures)
// or a restored one (url → fetch). Returns '' on failure.
async function refToBase64(r) {
try {
if (r.file) {
const dataUrl = await new Promise((res, rej) => {
const fr = new FileReader()
fr.onload = () => res(fr.result)
fr.onerror = rej
fr.readAsDataURL(r.file)
})
return dataUrl.replace(/^data:[^,]*,/, '')
}
if (r.dataUrl) return r.dataUrl.replace(/^data:[^,]*,/, '')
if (r.url) {
const blob = await (await fetch(r.url)).blob()
@@ -650,7 +663,7 @@ onUnmounted(() => {
<label class="block text-xs font-medium text-slate-500 mb-1.5">
参考图
<span class="text-slate-400 font-normal">
(最多 {{ maxRefs }} 张{{ refMode === 'frame' && mode === 'video' ? (maxRefs >= 2 ? ' · 首帧/末帧' : ' · 首帧') : '' }} · 单张 ≤8MB)
(最多 {{ maxRefs }} 张{{ refMode === 'frame' && mode === 'video' ? (maxRefs >= 2 ? ' · 首帧/末帧' : ' · 首帧') : '' }} · 单张 ≤20MB)
</span>
<span v-if="refsRequired" class="text-rose-500">*</span>
</label>
@@ -659,7 +672,7 @@ onUnmounted(() => {
@drop="onDrop" @dragover="onDragOver" @dragleave="onDragLeave">
<div v-for="(img, i) in refImages" :key="i"
class="relative w-20 h-20 rounded-lg overflow-hidden border border-slate-200 bg-slate-50 transition-all">
<img :src="img.dataUrl || img.url" class="w-full h-full object-cover" />
<img :src="img.thumb || img.dataUrl || img.url" class="w-full h-full object-cover" />
<button type="button" @click="removeRef(i)"
class="absolute top-1 right-1 w-5 h-5 rounded-full bg-slate-900/70 text-white hover:bg-rose-500 grid place-items-center disabled:opacity-40 disabled:cursor-not-allowed">
<Icon name="close" class="w-3 h-3" />