Skip to content

lib-x/ask4me

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ask4me — Go SDK

Go SDK for the Ask4Me API — a self-hosted Human-in-the-Loop service.

Official server repo: github.com/easychen/ask4me
Official website: ask4me.ft07.com

What is Ask4Me?

Ask4Me turns an HTTP request into a human interaction:

  1. Your code calls POST /v1/ask with a title, body, and optional buttons / form.
  2. The server pushes a notification (ServerChan / Apprise) with an interaction link.
  3. The user opens the link, clicks a button or types a reply.
  4. Your request returns the result.

Installation

go get github.com/lib-x/ask4me

Start the server

npm install -g ask4me-server
ask4me-server --config ./.env

Minimum .env:

ASK4ME_BASE_URL=https://your-domain.com
ASK4ME_API_KEY=change-me
ASK4ME_SERVERCHAN_SENDKEY=your-sendkey   # or ASK4ME_APPRISE_URLS
ASK4ME_TERMINAL_CACHE_SECONDS=300

Quick Start

func main() {
	const (
		endpoint = "http://localhost:8080/v1/ask"
		apiKey   = "change-me"
	)

	// Build the MCD using the fluent helper.
	mcd := ask4me.NewMCD().
		Buttons(
			ask4me.Button{Label: "OK", Value: "ok"},
			ask4me.Button{Label: "Later", Value: "later"},
		).
		Input("note", "补充说明", "提交").
		Build()

	result, err := ask4me.Ask(ask4me.AskOptions{
		Endpoint: endpoint,
		APIKey:   apiKey,
		// Stream: false is the default — uses nonStream mode.
		Payload: &ask4me.Payload{
			Title:            "Ask4Me Demo",
			Body:             "这是一条来自 Ask4Me 的测试消息,请随便点一个按钮或回一段话。",
			MCD:              mcd,
			ExpiresInSeconds: 600,
		},
	})
	if err != nil {
		log.Fatalf("ask failed: %v", err)
	}

	out, _ := json.MarshalIndent(json.RawMessage(result.Result.Raw), "", "  ")
	fmt.Println(string(out))
}

Modes

nonStream (default, recommended)

The call blocks until the user submits or the request expires, then returns a single JSON response. No streaming complexity required.

result, err := ask4me.Ask(ask4me.AskOptions{
    Endpoint: "http://localhost:8080/v1/ask",
    APIKey:   "change-me",
    // Stream: false  ← default
    Payload: &ask4me.Payload{
        Title:            "Approve?",
        Body:             "Please confirm the production deployment.",
        MCD:              ask4me.NewMCD().Buttons(
                              ask4me.Button{Label: "Deploy", Value: "deploy"},
                              ask4me.Button{Label: "Abort",  Value: "abort"},
                          ).Build(),
        ExpiresInSeconds: 600,
    },
})
// result.Result is the terminal event

SSE stream mode

Receive events in real time. The request.created event includes interaction_url — useful if you want to display or log the link directly rather than waiting for the notification channel.

result, err := ask4me.Ask(ask4me.AskOptions{
    Endpoint: endpoint,
    APIKey:   apiKey,
    Stream:   true,
    Payload:  &ask4me.Payload{ /* … */ },
    OnEvent: func(ev *ask4me.Event) {
        fmt.Printf("[%s] id=%s url=%s\n", ev.Type, ev.ID, ev.InteractionURL)
    },
})

MCD builder

mcd := ask4me.NewMCD().
    Buttons(
        ask4me.Button{Label: "OK",    Value: "ok"},
        ask4me.Button{Label: "Later", Value: "later"},
    ).
    Input("note", "Leave a note", "Submit").
    Build()

Or write the string by hand:

:::buttons
- [OK](ok)
- [Later](later)
:::

:::input name="note" label="Leave a note" submit="Submit"
:::

JSON Forms (schema-driven form)

JSONForms is POST-only. When set, it overrides MCD.

result, err := ask4me.Ask(ask4me.AskOptions{
    Endpoint: endpoint,
    APIKey:   apiKey,
    Payload: &ask4me.Payload{
        Title: "User info",
        Body:  "Please fill in the form.",
        JSONForms: &ask4me.JSONFormsConfig{
            Schema:      json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}`),
            SubmitLabel: "Submit",
        },
        ExpiresInSeconds: 600,
    },
})

// form data is in result.Result.Data.Payload

Resuming a dropped connection

// First attempt — may time out before user responds.
result, err := ask4me.Ask(ask4me.AskOptions{
    Endpoint:  endpoint,
    APIKey:    apiKey,
    RequestID: "req_myjob_001", // pre-generated; must start with "req_"
    Payload: &ask4me.Payload{
        RequestID: "req_myjob_001",
        Title:     "Confirm?",
        /* … */
    },
})

// Resume later — body is not re-sent when RequestID is provided.
result, err = ask4me.Ask(ask4me.AskOptions{
    Endpoint:  endpoint,
    APIKey:    apiKey,
    RequestID: "req_myjob_001",
})

GET-style auth (AuthQueryKey)

When you cannot set HTTP headers, pass the API key as a ?key= query parameter. Note that query params may appear in proxy/server logs.

result, err := ask4me.Ask(ask4me.AskOptions{
    Endpoint: endpoint,
    APIKey:   apiKey,
    AuthMode: ask4me.AuthQueryKey, // adds ?key=... to the URL
    Payload:  &ask4me.Payload{ /* … */ },
})

Reusable Client

client, err := ask4me.NewClient("http://localhost:8080/v1/ask", "change-me")
if err != nil { log.Fatal(err) }

result, err := client.Ask(ask4me.AskOptions{
    Payload: &ask4me.Payload{ /* … */ },
})

API reference

AskOptions

Field Type Default Description
Endpoint string Required. Full API URL
APIKey string Required. Bearer token
AuthMode AuthMode AuthBearer AuthBearer or AuthQueryKey
Payload *Payload nil Typed request payload
RawPayload any nil Untyped payload (used when Payload is nil)
Stream bool false true = SSE mode, false = nonStream
OnEvent func(*Event) nil Per-event callback (SSE mode only)
Context context.Context Background() Cancellation / timeout
MaxRetries int 0 (unlimited) Max reconnect attempts
InitialBackoff time.Duration 200 ms Starting retry sleep
MaxBackoff time.Duration 2 s Retry sleep ceiling
RequestID string Resume an existing request
LastEventID string Resume SSE from a specific event
HTTPClient *http.Client http.DefaultClient Custom HTTP client

Payload

Field Type Description
Title string Notification title
Body string Notification body
MCD string Interaction control description (buttons / input)
JSONForms *JSONFormsConfig Schema-driven form (POST only, overrides MCD)
ExpiresInSeconds int TTL for the request
RequestID string Pre-generated ID (must start with req_)
ServerChanActionLinks bool Enable ServerChan 3 Action Links (requires https base URL)

AskResult

type AskResult struct {
    RequestID   string   // server-assigned request ID
    LastEventID string   // last SSE event ID (for resuming)
    Events      []*Event // all SSE events in order (SSE mode)
    Result      *Event   // terminal event
}

Event

type Event struct {
    Type           string          // "request.created", "user.submitted", "request.expired", "notify.failed"
    ID             string          // event ID
    RequestID      string          // request ID
    InteractionURL string          // set on "request.created" (SSE only)
    Data           *SubmitData     // set on terminal events
    Raw            json.RawMessage // full original JSON
}

func (e *Event) IsTerminal() bool  // true for user.submitted / request.expired / notify.failed

SubmitData

type SubmitData struct {
    Action  string          // button value clicked
    Text    string          // text typed by user
    Payload json.RawMessage // form data when JSONForms was used
}

Terminal event types

Type Meaning
user.submitted User clicked a button or submitted the form
request.expired Timed out without submission
notify.failed Notification delivery failed

Examples

Directory Description
examples/nonstream Default mode — block until result
examples/stream SSE mode — receive events in real time
examples/jsonforms Schema-driven form
examples/resume Pre-generated request_id + connection resume
examples/querykey GET-style auth without Authorization header

Links

Releases

No releases published

Packages

 
 
 

Contributors

Languages