164 lines
4.3 KiB
Go
164 lines
4.3 KiB
Go
package questiontable
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// Field IDs for the DingTalk Notable v1 question table.
|
|
// Verified against the live Notable v1 base.
|
|
const (
|
|
FieldChineseQuestion = "ZaMZF6j" // text
|
|
FieldAskedTime = "SCug9My" // date (Unix milliseconds)
|
|
FieldAsker = "S2XA1uZ" // user (array of {name, unionId})
|
|
FieldOriginalQuestion = "bETcL3r" // text
|
|
FieldAnswer = "Y4cpmxU" // text
|
|
FieldTriggerPerson = "JgY2f1z" // text
|
|
FieldLanguage = "DNIV3mQ" // text
|
|
FieldDepartment = "eBc6a6b" // text
|
|
FieldRegion = "fPNLeVW" // text
|
|
)
|
|
|
|
// Row holds the data needed to produce a Notable v1 typed record.
|
|
type Row struct {
|
|
Question string
|
|
Answer string
|
|
AskedTime time.Time
|
|
AskerName string
|
|
AskerUnionID string
|
|
TriggerPerson string
|
|
Department string
|
|
Region string
|
|
QuestionImageURLs []string
|
|
}
|
|
|
|
// ToFields returns a typed field-ID → value map suitable for the Notable v1
|
|
// InsertRecord request body. The caller is responsible for the semantics of
|
|
// each field; this method only handles mapping, language detection, time
|
|
// formatting, and formula-injection sanitisation.
|
|
// The five v1 select fields (QMn9UoU, H3gtTuv, nFvv8OR, UMqUPAG, lnF7gF9)
|
|
// are omitted entirely.
|
|
func (r Row) ToFields() map[string]interface{} {
|
|
fields := make(map[string]interface{})
|
|
|
|
lang := detectLanguage(r.Question)
|
|
|
|
// ChineseQuestion: only when Han present.
|
|
if lang == "中文" {
|
|
fields[FieldChineseQuestion] = sanitise(r.Question)
|
|
}
|
|
|
|
// Answer.
|
|
fields[FieldAnswer] = sanitise(r.Answer)
|
|
|
|
// AskedTime: Unix milliseconds if non-zero; omit if zero.
|
|
if !r.AskedTime.IsZero() {
|
|
fields[FieldAskedTime] = r.AskedTime.UnixMilli()
|
|
}
|
|
|
|
// Asker: user object only when AskerUnionID is non-blank.
|
|
if strings.TrimSpace(r.AskerUnionID) != "" {
|
|
fields[FieldAsker] = []interface{}{
|
|
map[string]interface{}{
|
|
"name": strings.TrimSpace(r.AskerName),
|
|
"unionId": strings.TrimSpace(r.AskerUnionID),
|
|
},
|
|
}
|
|
}
|
|
|
|
// OriginalQuestion: include image marker and stable URLs for image turns.
|
|
fields[FieldOriginalQuestion] = sanitise(questionWithImageURLs(r.Question, r.QuestionImageURLs))
|
|
|
|
// Language.
|
|
fields[FieldLanguage] = sanitise(lang)
|
|
|
|
// TriggerPerson: fallback to AskerName.
|
|
triggerPerson := r.TriggerPerson
|
|
if triggerPerson == "" {
|
|
triggerPerson = r.AskerName
|
|
}
|
|
fields[FieldTriggerPerson] = sanitise(triggerPerson)
|
|
|
|
// Department.
|
|
fields[FieldDepartment] = sanitise(r.Department)
|
|
|
|
// Region.
|
|
fields[FieldRegion] = sanitise(r.Region)
|
|
|
|
return fields
|
|
}
|
|
|
|
// detectLanguage returns "中文" when s contains any Han character,
|
|
// "英文" when it contains Latin letters but no Han,
|
|
// and "其他" otherwise.
|
|
func detectLanguage(s string) string {
|
|
hasHan := false
|
|
hasLatin := false
|
|
for _, r := range s {
|
|
if isHan(r) {
|
|
hasHan = true
|
|
}
|
|
if isLatin(r) {
|
|
hasLatin = true
|
|
}
|
|
}
|
|
if hasHan {
|
|
return "中文"
|
|
}
|
|
if hasLatin {
|
|
return "英文"
|
|
}
|
|
return "其他"
|
|
}
|
|
|
|
func isHan(r rune) bool {
|
|
return unicode.Is(unicode.Han, r)
|
|
}
|
|
|
|
func isLatin(r rune) bool {
|
|
return unicode.Is(unicode.Latin, r)
|
|
}
|
|
|
|
// sanitise protects against spreadsheet formula injection. If the string
|
|
// begins with =, +, -, or @, a single apostrophe is prefixed. The string
|
|
// is otherwise returned unchanged.
|
|
func sanitise(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
r, _ := utf8.DecodeRuneInString(s)
|
|
switch r {
|
|
case '=', '+', '-', '@':
|
|
return "'" + s
|
|
}
|
|
return s
|
|
}
|
|
|
|
// questionWithImageURLs prefixes an image marker and renders the stable image
|
|
// URLs as markdown image syntax when the turn carried image input. Markdown
|
|
// image syntax lets the Notable cell preview the image (same as the answer
|
|
// column); a plain-text URL would render as an unpreviewable link.
|
|
func questionWithImageURLs(question string, imageURLs []string) string {
|
|
if len(imageURLs) == 0 {
|
|
return question
|
|
}
|
|
var builder strings.Builder
|
|
builder.WriteString("[图片] ")
|
|
builder.WriteString(strings.TrimSpace(question))
|
|
for index, imageURL := range imageURLs {
|
|
imageURL = strings.TrimSpace(imageURL)
|
|
if imageURL == "" {
|
|
continue
|
|
}
|
|
builder.WriteString("\n
|
|
builder.WriteString(imageURL)
|
|
builder.WriteString(")")
|
|
}
|
|
return builder.String()
|
|
}
|