fix: improve ai table file handling
This commit is contained in:
+171
-9
@@ -132,6 +132,15 @@ https://{host}/v1/workbench/ai/*
|
||||
| `analyze_trends` | 分析趋势变化 |
|
||||
| `generate_report` | 生成解读报告 |
|
||||
|
||||
#### 0.4.6 AI 推送结果类型
|
||||
|
||||
| 枚举 | 说明 |
|
||||
| --- | --- |
|
||||
| `text` | 文本结果,适用于 AI 美文、日报/月报润色、较短的智能文档草稿 |
|
||||
| `file` | 文件结果,适用于导出 docx、pdf 等文件;推送中返回文件服务下载地址 |
|
||||
| `record` | 记录结果,适用于智能表格、智能文档等已生成记录的能力 |
|
||||
| `summary` | 会议总结类结果,挂在会议下展示 |
|
||||
|
||||
### 0.5 通用错误码
|
||||
|
||||
| reason | HTTP | 说明 | 前端处理 |
|
||||
@@ -161,6 +170,8 @@ https://{host}/v1/workbench/ai/*
|
||||
| 弱网重试 | 写接口使用同一个 `Idempotency-Key` 重试 |
|
||||
| 取消任务 | 只允许取消 `queued`、`processing` 状态任务 |
|
||||
| 记录保存 | 智能文档生成成功后,必须显式保存才进入文档记录;智能表格解读成功后默认生成记录 |
|
||||
| 在线推送 | AI 任务完成后仅推送当前在线用户;离线用户不补推,上线后通过任务或记录接口重新拉取 |
|
||||
| 推送兜底 | 前端接入推送后仍需保留轮询和页面恢复拉取,推送只作为实时体验优化 |
|
||||
|
||||
## 1. 工作台 AI 入口与应用搜索
|
||||
|
||||
@@ -1046,6 +1057,23 @@ Idempotency-Key: ai_cancel_01K8ZC3FZ6BTX9Z1SK8HV7QYWM
|
||||
| `cursor` | string | 否 | 分页游标 |
|
||||
| `limit` | number | 否 | 默认 20,最大 50 |
|
||||
|
||||
#### 出参说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `items[].record_id` | string | 解读记录 ID |
|
||||
| `items[].title` | string | 解读报告标题 |
|
||||
| `items[].summary` | string | 解读摘要,列表页可展示前几行 |
|
||||
| `items[].format` | string | 展示用文件格式,取值为小写文件后缀,例如 `xlsx`、`xls`、`csv` |
|
||||
| `items[].file_type` | string | 原始文件类型,当前与 `format` 一致,保留给前端逻辑判断 |
|
||||
| `items[].file_name` | string | 原始文件名 |
|
||||
| `items[].file_size` | number | 原始文件大小,单位 byte |
|
||||
| `items[].status` | string | 解读状态,见 0.4.2 |
|
||||
| `items[].created_at` | string | 创建时间,UTC RFC3339 |
|
||||
| `items[].updated_at` | string | 更新时间,UTC RFC3339 |
|
||||
| `next_cursor` | string | 下一页游标,为空表示无下一页 |
|
||||
| `has_more` | boolean | 是否还有下一页 |
|
||||
|
||||
#### 出参示例
|
||||
|
||||
```json
|
||||
@@ -1056,10 +1084,14 @@ Idempotency-Key: ai_cancel_01K8ZC3FZ6BTX9Z1SK8HV7QYWM
|
||||
{
|
||||
"record_id": "airec_tbl_6LP0nM9s3B",
|
||||
"title": "8月销售数据",
|
||||
"summary": "本次报表展示 8 月销售数据的核心指标、异常波动和区域表现。",
|
||||
"format": "xlsx",
|
||||
"file_name": "sales_august.xlsx",
|
||||
"file_type": "xlsx",
|
||||
"file_size": 18146135,
|
||||
"status": "succeeded",
|
||||
"created_at": "2026-09-25T08:42:00.000Z"
|
||||
"created_at": "2026-09-25T08:42:00.000Z",
|
||||
"updated_at": "2026-09-25T08:42:00.000Z"
|
||||
}
|
||||
],
|
||||
"next_cursor": "",
|
||||
@@ -1534,31 +1566,150 @@ X-Request-ID: 01K8ZC3FZ6BTX9Z1SK8HV7QYWV
|
||||
|
||||
| 项 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | `可后续扩展` |
|
||||
| 事件 | `workbench.ai.task_updated` |
|
||||
| 用途 | 服务端主动推送 AI 任务状态变化,减少轮询 |
|
||||
| 状态 | `建议接入` |
|
||||
| 事件 | `workbench.ai.task.updated` |
|
||||
| 用途 | 服务端主动推送 AI 任务完成、失败或取消状态,减少前台等待 |
|
||||
| 鉴权 | 复用 IM WebSocket 登录态 |
|
||||
|
||||
#### 事件示例
|
||||
#### 推送边界
|
||||
|
||||
1. 仅推送在线用户。用户离线时服务端不保存离线 AI 推送、不补发历史推送。
|
||||
2. 用户重新上线、重新进入工作台 AI 页面时,前端通过任务详情、记录列表或业务详情接口恢复状态。
|
||||
3. 推送内容只放任务状态、结果索引和轻量结果;完整长文本、大对象和权限敏感内容仍以 HTTP 详情接口为准。
|
||||
4. 文件结果不推裸 OSS 签名地址,统一推 IM 文件服务稳定地址:`/v0/file/s/{file_id}` 或其绝对 URL。
|
||||
5. 推送丢失、WebSocket 断线、App 切后台等场景下,前端必须保留轮询兜底。
|
||||
|
||||
#### 通用事件结构
|
||||
|
||||
```json
|
||||
{
|
||||
"topic": "workbench.ai.task_updated",
|
||||
"topic": "workbench.ai.task.updated",
|
||||
"data": {
|
||||
"task_id": "aitask_tbl_2Fh9aQx73K",
|
||||
"feature": "smart_table",
|
||||
"status": "succeeded",
|
||||
"progress": 100,
|
||||
"result_type": "record",
|
||||
"record_id": "airec_tbl_6LP0nM9s3B",
|
||||
"result": {
|
||||
"title": "8月销售数据解读",
|
||||
"summary": "销售额整体平稳,建议重点关注渠道结构和异常订单。"
|
||||
},
|
||||
"updated_at": "2026-09-25T08:42:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `task_id` | string | 是 | AI 任务 ID |
|
||||
| `feature` | string | 是 | AI 能力类型,见 0.4.1 |
|
||||
| `status` | string | 是 | `succeeded`、`failed`、`canceled` 为主要推送状态 |
|
||||
| `progress` | number | 否 | 进度,完成态一般为 `100` |
|
||||
| `result_type` | string | 否 | `text`、`file`、`record`、`summary` |
|
||||
| `record_id` | string | 否 | 记录类结果 ID |
|
||||
| `meeting_id` | string | 否 | 会议总结所属会议 ID |
|
||||
| `summary_id` | string | 否 | 会议总结 ID |
|
||||
| `result` | object | 否 | 轻量结果体,按 result_type 区分 |
|
||||
| `error` | object | 否 | 失败信息,包含 `reason`、`message` |
|
||||
| `updated_at` | string | 是 | 更新时间,UTC RFC3339 |
|
||||
|
||||
#### result 示例
|
||||
|
||||
文本结果:
|
||||
|
||||
```json
|
||||
{
|
||||
"result_type": "text",
|
||||
"result": {
|
||||
"text": "润色后的文本内容",
|
||||
"scene": "daily_report_polish"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
文件结果:
|
||||
|
||||
```json
|
||||
{
|
||||
"result_type": "file",
|
||||
"result": {
|
||||
"file_id": "IHC4k7rlpo0.docx",
|
||||
"file_name": "阿德勒-解读报告.docx",
|
||||
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"file_size": 2870,
|
||||
"download_url": "https://api.118im.com/v0/file/s/IHC4k7rlpo0.docx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
记录结果:
|
||||
|
||||
```json
|
||||
{
|
||||
"result_type": "record",
|
||||
"record_id": "airec_tbl_6LP0nM9s3B",
|
||||
"result": {
|
||||
"title": "8月销售数据解读",
|
||||
"summary": "销售额整体平稳,建议关注渠道结构。"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
会议总结结果:
|
||||
|
||||
```json
|
||||
{
|
||||
"result_type": "summary",
|
||||
"meeting_id": "mtg_7YUHoLTqrl4ZO_G6",
|
||||
"summary_id": "aisum_3Rn8qU2b6X",
|
||||
"result": {
|
||||
"conclusion_count": 3,
|
||||
"action_item_count": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
失败结果:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "aitask_tbl_2Fh9aQx73K",
|
||||
"feature": "smart_table",
|
||||
"status": "failed",
|
||||
"progress": 100,
|
||||
"error": {
|
||||
"reason": "AI_TASK_FAILED",
|
||||
"message": "生成失败,请重试"
|
||||
},
|
||||
"updated_at": "2026-09-25T08:42:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### 各接口推送建议
|
||||
|
||||
| 接口/能力 | 是否异步 | 是否推送 | result_type | 推送内容建议 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `POST /v1/workbench/ai/documents/generate` 智能文档生成 | 是 | 是 | `text` 或 `record` | 内容较短可直接推 `title`、`content_plain`、`content_markdown`;内容较长只推 `task_id` 和摘要,前端拉任务详情 |
|
||||
| `POST /v1/workbench/ai/documents` 保存文档 | 否 | 否 | - | 用户主动保存,HTTP 返回即可 |
|
||||
| `POST /v1/workbench/ai/table-analyses` 智能表格解读 | 是 | 是 | `record` | 推 `record_id`、标题、摘要;前端可直接展示完成卡片,点开时拉详情 |
|
||||
| `POST /v1/workbench/ai/table-analyses/{record_id}/export` 导出报告 | 可异步 | 是 | `file` | 推 `file_id`、`file_name`、`mime_type`、`file_size`、`download_url` |
|
||||
| `POST /v1/workbench/ai/text/polish` AI 美文 | 可同步;超过 2 秒建议异步 | 异步时推 | `text` | 推润色后的 `text`,内容过长时只推摘要并拉任务详情 |
|
||||
| 日报 AI 润色 | 可同步;超过 2 秒建议异步 | 异步时推 | `text` | 推润色后的日报内容或草稿 ID |
|
||||
| 月报 AI 润色 | 可同步;超过 2 秒建议异步 | 异步时推 | `text` | 推 `work_summary`、`next_plan` 或草稿 ID |
|
||||
| `POST /v1/workbench/meetings/{meeting_id}/ai-summary` 会议 AI 总结 | 是 | 是 | `summary` | 推 `meeting_id`、`summary_id`、结论数、待办数;完整内容拉详情 |
|
||||
| `GET /v1/workbench/ai/tasks/{task_id}` 查询任务 | 否 | 否 | - | 前端主动拉取,作为推送兜底 |
|
||||
| `POST /v1/workbench/ai/tasks/{task_id}/cancel` 取消任务 | 否 | 可选 | - | 多端在线时可推 `status=canceled`;单端场景可只依赖 HTTP 返回 |
|
||||
|
||||
#### 对接重点
|
||||
|
||||
1. 首版建议优先使用 HTTP 轮询,WebSocket 事件作为后续优化。
|
||||
2. 即使接入事件,前端仍需保留轮询兜底。
|
||||
1. 推送用于“在线即时更新”,不是最终数据源;最终状态以 HTTP 查询为准。
|
||||
2. 前端收到 `succeeded` 后,可优先使用推送中的轻量结果更新 UI;需要完整内容时再拉详情。
|
||||
3. 前端收到 `file` 类型结果后,可直接展示下载按钮,下载地址使用 `download_url`。
|
||||
4. 前端收到 `failed` 后展示失败并允许重试,不展示供应商原始错误。
|
||||
5. App 启动、WebSocket 重连、进入 AI 页面时,前端重新拉任务或记录列表恢复离线期间完成的任务。
|
||||
|
||||
## 7. 对接重难点
|
||||
|
||||
@@ -1568,7 +1719,18 @@ AI 接口只接受 IM 登录态,不单独做 AI 登录。租户、用户、部
|
||||
|
||||
### 7.2 异步任务与用户体验
|
||||
|
||||
智能文档、智能表格、会议 AI 总结都可能耗时。前端必须处理 `queued`、`processing`、`succeeded`、`failed`、`canceled`,并在用户离开页面后可通过记录或详情恢复结果。
|
||||
智能文档、智能表格、会议 AI 总结都可能耗时。前端必须处理 `queued`、`processing`、`succeeded`、`failed`、`canceled`,并在用户离开页面后可通过任务、记录或业务详情恢复结果。
|
||||
|
||||
推荐交互流程:
|
||||
|
||||
1. 创建任务接口立即返回 `task_id`、`status=queued`、`polling_interval_ms`,前端进入 loading 或后台任务状态。
|
||||
2. 用户在线时,服务端通过 IM WebSocket 推送 `workbench.ai.task.updated`,前端收到完成事件后直接刷新 UI。
|
||||
3. 用户离线、App 切后台、WebSocket 断线时,服务端不补发离线 AI 推送;前端重新上线后主动拉任务详情、AI 记录列表或业务详情。
|
||||
4. 推送结果为 `text` 时,短文本可直接展示;长文本以 HTTP 任务详情为准。
|
||||
5. 推送结果为 `file` 时,前端直接使用 `download_url` 展示下载入口;该地址应为 IM 文件服务稳定地址,不是裸 OSS 签名地址。
|
||||
6. 推送结果为 `record` 或 `summary` 时,前端可先展示标题和摘要,点开详情时再请求对应详情接口。
|
||||
7. 同一账号多端在线时,所有在线端都可以收到任务完成推送;各端仍需按页面状态决定是否自动展示或只更新角标。
|
||||
8. 失败、取消也应更新本地任务状态;失败文案按 `error.reason` 做本地化,不展示供应商原始报错。
|
||||
|
||||
### 7.3 文件服务与 AI 服务衔接
|
||||
|
||||
|
||||
+26
-2
@@ -379,7 +379,7 @@ func (a *API) aiTableAnalysisCreate(w http.ResponseWriter, r *http.Request, p *a
|
||||
writeStoreError(w, r, err)
|
||||
return
|
||||
}
|
||||
profile := tableProfileForAI(resolveTableFilePath(fileID, fileMeta.Location), fileType)
|
||||
profile := a.tableProfileForAI(r, fileID, fileMeta, fileType)
|
||||
result, err := a.ai.AnalyzeTable(r.Context(), ai.TableAnalysisInput{
|
||||
Topic: topic, FileID: fileID, FileName: strings.TrimSpace(body.FileName), FileType: fileType, FileSize: fileSize, Profile: profile,
|
||||
})
|
||||
@@ -831,10 +831,15 @@ func aiRecordPayloads(records []store.AIRecord) []map[string]any {
|
||||
func aiTableRecordPayloads(records []store.AIRecord) []map[string]any {
|
||||
items := make([]map[string]any, 0, len(records))
|
||||
for _, record := range records {
|
||||
fileType := strings.ToLower(strings.TrimSpace(record.FileType))
|
||||
items = append(items, map[string]any{
|
||||
"record_id": record.RecordID,
|
||||
"title": record.Title,
|
||||
"summary": record.Summary,
|
||||
"format": fileType,
|
||||
"file_type": fileType,
|
||||
"file_name": record.FileName,
|
||||
"file_size": record.FileSize,
|
||||
"status": record.Status,
|
||||
"created_at": record.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||
"updated_at": record.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||
@@ -976,7 +981,26 @@ func resolveTableFilePath(fileID, location string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func tableProfileForAI(path, fileType string) ai.TableProfile {
|
||||
func (a *API) tableProfileForAI(r *http.Request, fileID string, meta store.AIFileMetadata, fileType string) ai.TableProfile {
|
||||
if path := resolveTableFilePath(fileID, meta.Location); path != "" {
|
||||
return tableProfileFromPath(path, fileType)
|
||||
}
|
||||
if path, cleanup, err := downloadWorkbenchFileToTemp(r, fileID, fileType); err == nil {
|
||||
defer cleanup()
|
||||
return tableProfileFromPath(path, fileType)
|
||||
}
|
||||
if strings.TrimSpace(meta.Location) == "" {
|
||||
return ai.TableProfile{Parsed: false, Message: "source file metadata is not available"}
|
||||
}
|
||||
path, cleanup, err := a.downloadOSSObjectToTemp(r.Context(), meta.Location, fileType)
|
||||
if err != nil {
|
||||
return ai.TableProfile{Parsed: false, Message: "source file is not available: " + err.Error()}
|
||||
}
|
||||
defer cleanup()
|
||||
return tableProfileFromPath(path, fileType)
|
||||
}
|
||||
|
||||
func tableProfileFromPath(path, fileType string) ai.TableProfile {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return ai.TableProfile{Parsed: false, Message: "source file is not available locally"}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -88,6 +91,98 @@ func (a *API) uploadAITableAnalysisExport(ctx context.Context, r *http.Request,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func downloadWorkbenchFileToTemp(r *http.Request, fileID, fileType string) (string, func(), error) {
|
||||
fileID = strings.TrimSpace(fileID)
|
||||
if fileID == "" {
|
||||
return "", func() {}, fmt.Errorf("empty file id")
|
||||
}
|
||||
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, absoluteURL(r, "/v0/file/s/"+url.PathEscape(fileID)), nil)
|
||||
if err != nil {
|
||||
return "", func() {}, err
|
||||
}
|
||||
copyHeader(req, r, "Authorization")
|
||||
copyHeader(req, r, "X-MeetingIM-APIKey")
|
||||
copyHeader(req, r, "X-Tenant-Code")
|
||||
copyHeader(req, r, "X-IM-Schema")
|
||||
copyHeader(req, r, "X-Request-ID")
|
||||
if auth := strings.TrimSpace(r.Header.Get("X-MeetingIM-Auth")); auth != "" {
|
||||
req.Header.Set("X-MeetingIM-Auth", auth)
|
||||
} else if token := bearerTokenFromHeader(r.Header.Get("Authorization")); token != "" {
|
||||
req.Header.Set("X-MeetingIM-Auth", "token "+token)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", func() {}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", func() {}, fmt.Errorf("file service returned %d", resp.StatusCode)
|
||||
}
|
||||
return responseBodyToTemp(resp.Body, fileType)
|
||||
}
|
||||
|
||||
func (a *API) downloadOSSObjectToTemp(ctx context.Context, objectKey, fileType string) (string, func(), error) {
|
||||
objectKey = strings.Trim(strings.TrimSpace(objectKey), "/")
|
||||
if objectKey == "" {
|
||||
return "", func() {}, fmt.Errorf("empty object key")
|
||||
}
|
||||
cfg, err := a.ossUploadConfig(ctx)
|
||||
if err != nil {
|
||||
return "", func() {}, err
|
||||
}
|
||||
client, err := aliyunoss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
|
||||
if err != nil {
|
||||
return "", func() {}, err
|
||||
}
|
||||
bucket, err := client.Bucket(cfg.Bucket)
|
||||
if err != nil {
|
||||
return "", func() {}, err
|
||||
}
|
||||
reader, err := bucket.GetObject(objectKey)
|
||||
if err != nil {
|
||||
return "", func() {}, err
|
||||
}
|
||||
defer reader.Close()
|
||||
return responseBodyToTemp(reader, fileType)
|
||||
}
|
||||
|
||||
func responseBodyToTemp(reader io.Reader, fileType string) (string, func(), error) {
|
||||
suffix := "." + strings.TrimLeft(strings.ToLower(strings.TrimSpace(fileType)), ".")
|
||||
if suffix == "." {
|
||||
suffix = ".table"
|
||||
}
|
||||
tmp, err := os.CreateTemp("", "im-workbench-ai-table-*"+suffix)
|
||||
if err != nil {
|
||||
return "", func() {}, err
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
cleanup := func() { _ = os.Remove(tmpPath) }
|
||||
if _, err := io.Copy(tmp, reader); err != nil {
|
||||
_ = tmp.Close()
|
||||
cleanup()
|
||||
return "", func() {}, err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
cleanup()
|
||||
return "", func() {}, err
|
||||
}
|
||||
return tmpPath, cleanup, nil
|
||||
}
|
||||
|
||||
func copyHeader(dst *http.Request, src *http.Request, key string) {
|
||||
if value := strings.TrimSpace(src.Header.Get(key)); value != "" {
|
||||
dst.Header.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func bearerTokenFromHeader(value string) string {
|
||||
token, ok := strings.CutPrefix(strings.TrimSpace(value), "Bearer ")
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(token)
|
||||
}
|
||||
|
||||
func (a *API) ossUploadConfig(ctx context.Context) (ossUploadConfig, error) {
|
||||
values, err := managerconfig.New(a.manager).ThirdPartyConfig(ctx, "im_third_party",
|
||||
"oss_enabled",
|
||||
|
||||
+31
-1
@@ -53,6 +53,9 @@ type AIRecord struct {
|
||||
Status string `db:"status"`
|
||||
DetailID string `db:"detail_id"`
|
||||
TaskID string `db:"task_id"`
|
||||
FileName string `db:"file_name"`
|
||||
FileType string `db:"file_type"`
|
||||
FileSize int64 `db:"file_size"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
DeletedAt *time.Time `db:"deleted_at"`
|
||||
@@ -356,7 +359,34 @@ func (s *Store) UpdateAITableAnalysisExportFileID(ctx context.Context, userID in
|
||||
}
|
||||
|
||||
func (s *Store) AITableAnalyses(ctx context.Context, userID int64, queryText, cursor string, limit int) ([]AIRecord, string, error) {
|
||||
return s.aiRecords(ctx, userID, "smart_table", queryText, cursor, limit)
|
||||
where := `r.tenant_id=? AND r.user_id=? AND r.record_type='smart_table' AND r.deleted_at IS NULL`
|
||||
args := []any{s.tenant(), formatInt64(userID)}
|
||||
if queryText != "" {
|
||||
where += ` AND (r.title LIKE ? OR r.summary LIKE ? OR a.file_name LIKE ?)`
|
||||
like := "%" + queryText + "%"
|
||||
args = append(args, like, like, like)
|
||||
}
|
||||
if cursor != "" {
|
||||
where += ` AND r.record_id<?`
|
||||
args = append(args, cursor)
|
||||
}
|
||||
args = append(args, limit+1)
|
||||
var items []AIRecord
|
||||
err := s.db.SelectContext(ctx, &items, `SELECT r.record_id,r.tenant_code,r.user_id,r.record_type,r.title,r.summary,r.status,r.detail_id,r.task_id,
|
||||
COALESCE(a.file_name, '') AS file_name,COALESCE(a.file_type, '') AS file_type,COALESCE(a.file_size, 0) AS file_size,
|
||||
r.created_at,r.updated_at,r.deleted_at
|
||||
FROM wb_ai_record r
|
||||
LEFT JOIN wb_ai_table_analysis a ON a.tenant_id=r.tenant_id AND a.record_id=r.detail_id
|
||||
WHERE `+where+` ORDER BY r.created_at DESC,r.record_id DESC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
next := ""
|
||||
if len(items) > limit {
|
||||
next = items[limit-1].RecordID
|
||||
items = items[:limit]
|
||||
}
|
||||
return items, next, nil
|
||||
}
|
||||
|
||||
func (s *Store) AIRecords(ctx context.Context, userID int64, recordType, queryText, cursor string, limit int) ([]AIRecord, string, error) {
|
||||
|
||||
@@ -92,12 +92,17 @@ func parseXLSX(path string) (Profile, error) {
|
||||
}
|
||||
defer f.Close()
|
||||
for _, sheet := range f.GetSheetList() {
|
||||
rows, err := f.GetRows(sheet)
|
||||
iter, err := f.Rows(sheet)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
cleaned := make([][]string, 0, min(len(rows), maxRows))
|
||||
for _, row := range rows {
|
||||
cleaned := make([][]string, 0, maxRows)
|
||||
for iter.Next() {
|
||||
row, err := iter.Columns()
|
||||
if err != nil {
|
||||
_ = iter.Close()
|
||||
return Profile{}, err
|
||||
}
|
||||
if emptyRow(row) {
|
||||
continue
|
||||
}
|
||||
@@ -106,6 +111,9 @@ func parseXLSX(path string) (Profile, error) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := iter.Close(); err != nil {
|
||||
return Profile{}, err
|
||||
}
|
||||
if len(cleaned) > 0 {
|
||||
return profileRows(cleaned), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user